[Re-open] Botany Rework Part 2: GrowthComponents (#39311)

* Restarting files from scratch

* reduce file count, more progress

* Additional partial work

* update 1

* update 2

* TODO execution

* fix

* Update PlantGrowthComponent.cs

* fix 1

* growth correction

* PlantGrowthComponent

* update 99

* last update

* oops

* Update vegan-meatball.yml

* Data loss

* ToxinsComponent

* Default components

* minor fix

* PlantTraitsComponent

* fix error

* fix error 2

* Copy growth components

* last update within this PR

* AutoHarvestGrowth remote

* Viable

* fix

* Fix growth rate

* Improvements to the YAML

* merge update

* fix

* code cleanup

* TODO

* fix

* oops

* oops 2

* more TODO

* fix names

* fix names oops

* Harvest bug fixes

* fix tests

* refactor: cleanup after mering master

* Update ConsumeExudeGasGrowthSystem.cs

* refactor: make this piece of happiness and ponies work

* review

* oops

* fix <summary>

* Separation of plant components into PlantComponent and PlantTraitsComponent and cleaning

* EnsureUniqueSeed

* refactor: change Resolve to TryComp where appropriate

* refactor: review comments - try get for components, clenaups

* refactor: minor yaml cleanups

* fixing trifles

---------

Co-authored-by: PraxisMapper <praxismapper@gmail.com>
Co-authored-by: pa.pecherskij <pa.pecherskij@interfax.ru>
This commit is contained in:
Princess Cheeseballs
2025-12-14 12:17:26 -08:00
committed by GitHub
44 changed files with 2950 additions and 2054 deletions

View File

@@ -0,0 +1,35 @@
using Content.Shared.Atmos;
namespace Content.Server.Botany.Components;
/// <summary>
/// Atmospheric-related requirements for proper entity growth. Used in botany.
/// </summary>
[RegisterComponent]
[DataDefinition]
public sealed partial class AtmosphericGrowthComponent : Component
{
/// <summary>
/// Ideal temperature for plant growth in Kelvin.
/// </summary>
[DataField]
public float IdealHeat = Atmospherics.T20C;
/// <summary>
/// Temperature tolerance range around <see cref="IdealHeat"/>.
/// </summary>
[DataField]
public float HeatTolerance = 10f;
/// <summary>
/// Minimum pressure tolerance for plant growth.
/// </summary>
[DataField]
public float LowPressureTolerance = 81f;
/// <summary>
/// Maximum pressure tolerance for plant growth.
/// </summary>
[DataField]
public float HighPressureTolerance = 121f;
}

View File

@@ -0,0 +1,21 @@
namespace Content.Server.Botany.Components;
/// <summary>
/// Basic parameters for plant growth.
/// </summary>
[RegisterComponent]
[DataDefinition]
public sealed partial class BasicGrowthComponent : Component
{
/// <summary>
/// Amount of water consumed per growth tick.
/// </summary>
[DataField]
public float WaterConsumption = 0.5f;
/// <summary>
/// Amount of nutrients consumed per growth tick.
/// </summary>
[DataField]
public float NutrientConsumption = 0.75f;
}

View File

@@ -1,19 +1,19 @@
using System.Threading;
namespace Content.Server.Botany.Components;
namespace Content.Server.Botany
/// <summary>
/// Anything that can be used to cross-pollinate plants.
/// </summary>
[RegisterComponent]
public sealed partial class BotanySwabComponent : Component
{
/// <summary>
/// Anything that can be used to cross-pollinate plants.
/// Delay between swab uses.
/// </summary>
[RegisterComponent]
public sealed partial class BotanySwabComponent : Component
{
[DataField("swabDelay")]
public float SwabDelay = 2f;
[DataField]
public TimeSpan SwabDelay = TimeSpan.FromSeconds(2);
/// <summary>
/// SeedData from the first plant that got swabbed.
/// </summary>
public SeedData? SeedData;
}
/// <summary>
/// SeedData from the first plant that got swabbed.
/// </summary>
public SeedData? SeedData;
}

View File

@@ -0,0 +1,21 @@
using Content.Shared.Atmos;
namespace Content.Server.Botany.Components;
/// <summary>
/// Data for gas to consume/exude on plant growth.
/// </summary>
[RegisterComponent]
[DataDefinition]
public sealed partial class ConsumeExudeGasGrowthComponent : Component
{
/// <summary>
/// Dictionary of gases and their consumption rates per growth tick.
/// </summary>
[DataField] public Dictionary<Gas, float> ConsumeGasses = new();
/// <summary>
/// Dictionary of gases and their exude rates per growth tick.
/// </summary>
[DataField] public Dictionary<Gas, float> ExudeGasses = new();
}

View File

@@ -0,0 +1,87 @@
using System.Linq;
using System.Reflection;
namespace Content.Server.Botany.Components;
/// <summary>
/// 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.
/// </summary>
[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();
/// <summary>
/// Extra-traits.
/// </summary>
[DataField]
public PlantTraitsComponent? PlantTraits { get; set; }
/// <summary>
/// Plant characteristics.
/// </summary>
[DataField]
public PlantComponent? Plant { get; set; }
/// <summary>
/// Basic properties for plant growth.
/// </summary>
[DataField]
public BasicGrowthComponent? BasicGrowth { get; set; }
/// <summary>
/// What defence plant have against toxins?
/// </summary>
[DataField]
public PlantToxinsComponent? Toxins { get; set; }
/// <summary>
/// Harvesting process-related data.
/// </summary>
[DataField]
public PlantHarvestComponent? Harvest { get; set; }
/// <summary>
/// Atmos-related environment requirements for plant growth.
/// </summary>
[DataField]
public AtmosphericGrowthComponent? AtmosphericGrowth { get; set; }
/// <summary>
/// What gases plant consume/exude upon growth.
/// </summary>
[DataField]
public ConsumeExudeGasGrowthComponent? ConsumeExudeGasGrowth { get; set; }
/// <summary>
/// Weeds and pests related data for plant.
/// </summary>
[DataField]
public WeedPestGrowthComponent? WeedPestGrowth { get; set; }
/// <summary>
/// Damage tolerance of plant.
/// </summary>
[DataField]
public UnviableGrowthComponent? UnviableGrowth { get; set; }
/// <summary>
/// Populates any null properties with default component instances so that
/// systems can always apply a full set. Existing (YAML-provided) values are kept.
/// </summary>
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);
}
}
}
}

View File

@@ -0,0 +1,51 @@
namespace Content.Server.Botany.Components;
/// <summary>
/// Component for storing core plant data.
/// </summary>
[RegisterComponent]
[DataDefinition]
public sealed partial class PlantComponent : Component
{
/// <summary>
/// The plant's max health.
/// </summary>
[DataField]
public float Endurance = 100f;
/// <summary>
/// How many produce are created on harvest.
/// </summary>
[DataField]
public int Yield;
/// <summary>
/// The number of growth ticks this plant can be alive for. Plants take high damage levels when Age > Lifespan.
/// </summary>
[DataField]
public float Lifespan;
/// <summary>
/// The number of growth ticks it takes for a plant to reach its final growth stage.
/// </summary>
[DataField]
public float Maturation;
/// <summary>
/// The number of growth ticks it takes for a plant to be (re-)harvestable. Shouldn't be lower than Maturation.
/// </summary>
[DataField]
public float Production;
/// <summary>
/// How many different sprites appear before the plant is fully grown.
/// </summary>
[DataField]
public int GrowthStages = 6;
/// <summary>
/// A scalar for sprite size and chemical solution volume in the produce. Caps at 100.
/// </summary>
[DataField]
public float Potency = 1f;
}

View File

@@ -0,0 +1,48 @@
namespace Content.Server.Botany.Components;
/// <summary>
/// Data for plant harvesting process.
/// </summary>
[RegisterComponent]
[DataDefinition]
public sealed partial class PlantHarvestComponent : Component
{
/// <summary>
/// Harvest repeat type.
/// </summary>
[DataField]
public HarvestType HarvestRepeat = HarvestType.NoRepeat;
/// <summary>
/// Whether the plant is currently ready for harvest.
/// </summary>
[ViewVariables]
public bool ReadyForHarvest = false;
/// <summary>
/// The age of the plant when last harvested.
/// </summary>
[ViewVariables]
public int LastHarvest = 0;
}
/// <summary>
/// Harvest options for plants.
/// </summary>
public enum HarvestType
{
/// <summary>
/// Plant is removed on harvest.
/// </summary>
NoRepeat,
/// <summary>
/// Plant makes produce every Production ticks.
/// </summary>
Repeat,
/// <summary>
/// Repeat, plus produce is dropped on the ground near the plant automatically.
/// </summary>
SelfHarvest
}

View File

@@ -4,6 +4,9 @@ using Robust.Shared.Audio;
namespace Content.Server.Botany.Components;
/// <summary>
/// Container for plant-holder and plant combined data.
/// </summary>
[RegisterComponent]
public sealed partial class PlantHolderComponent : Component
{
@@ -14,14 +17,8 @@ public sealed partial class PlantHolderComponent : Component
public TimeSpan NextUpdate = TimeSpan.Zero;
/// <summary>
/// Time between plant reagent consumption updates.
/// Number of missing gases required for plant growth.
/// </summary>
[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);
/// <summary>
/// Time between plant reagent consumption updates.
/// </summary>
[DataField]
public TimeSpan UpdateDelay = TimeSpan.FromSeconds(3);
/// <summary>
/// Game time when the plant last did a growth update.
/// </summary>
@@ -43,6 +46,9 @@ public sealed partial class PlantHolderComponent : Component
[DataField]
public SoundSpecifier? WateringSound;
/// <summary>
/// Whether to update the sprite after the next update cycle.
/// </summary>
[DataField]
public bool UpdateSpriteAfterUpdate;
@@ -53,33 +59,54 @@ public sealed partial class PlantHolderComponent : Component
[DataField]
public bool DrawWarnings = false;
/// <summary>
/// Current water level in the plant holder (0-100).
/// </summary>
[DataField]
public float WaterLevel = 100f;
/// <summary>
/// Current nutrient level in the plant holder (0-100).
/// </summary>
[DataField]
public float NutritionLevel = 100f;
/// <summary>
/// Current pest level in the plant holder (0-10).
/// </summary>
[DataField]
public float PestLevel;
/// <summary>
/// Current weed level in the plant holder (0-10).
/// </summary>
[DataField]
public float WeedLevel;
/// <summary>
/// Current toxin level in the plant holder (0-100).
/// </summary>
[DataField]
public float Toxins;
/// <summary>
/// Current age of the plant in growth cycles.
/// </summary>
[DataField]
public int Age;
/// <summary>
/// Number of growth cycles to skip due to poor conditions.
/// </summary>
[DataField]
public int SkipAging;
/// <summary>
/// Whether the plant is dead.
/// </summary>
[DataField]
public bool Dead;
[DataField]
public bool Harvest;
/// <summary>
/// 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;
/// <summary>
/// Multiplier for mutation chance and severity.
/// </summary>
[DataField]
public float MutationMod = 1f;
/// <summary>
/// Current mutation level (0-100).
/// </summary>
[DataField]
public float MutationLevel;
/// <summary>
/// Current health of the plant (0 to seed endurance).
/// </summary>
[DataField]
public float Health;
/// <summary>
/// Multiplier for weed growth rate.
/// </summary>
[DataField]
public float WeedCoefficient = 1f;
/// <summary>
/// Seed data for the currently planted seed.
/// </summary>
[DataField]
public SeedData? Seed;
@@ -120,12 +162,6 @@ public sealed partial class PlantHolderComponent : Component
[DataField]
public bool ImproperPressure;
/// <summary>
/// Not currently used.
/// </summary>
[DataField]
public bool ImproperLight;
/// <summary>
/// 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;
/// <summary>
/// Name of the solution container that holds the soil/nutrient solution.
/// </summary>
[DataField]
public string SoilSolutionName = "soil";
/// <summary>
/// Reference to the soil solution container.
/// </summary>
[ViewVariables]
public Entity<SolutionComponent>? SoilSolution = null;
}

View File

@@ -0,0 +1,21 @@
namespace Content.Server.Botany.Components;
/// <summary>
/// Data for plant resistance to toxins.
/// </summary>
[RegisterComponent]
[DataDefinition]
public sealed partial class PlantToxinsComponent : Component
{
/// <summary>
/// Maximum toxin level the plant can tolerate before taking damage.
/// </summary>
[DataField]
public float ToxinsTolerance = 4f;
/// <summary>
/// Divisor for calculating toxin uptake rate. Higher values mean slower toxin processing.
/// </summary>
[DataField]
public float ToxinUptakeDivisor = 10f;
}

View File

@@ -0,0 +1,49 @@
using Robust.Shared.Prototypes;
namespace Content.Server.Botany.Components;
/// <summary>
/// Component for managing special plant traits and mutations.
/// </summary>
/// 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
{
/// <summary>
/// If true, produce can't be put into the seed maker.
/// </summary>
[DataField]
public bool Seedless = false;
/// <summary>
/// If true, a sharp tool is required to harvest this plant.
/// </summary>
[DataField]
public bool Ligneous = false;
/// <summary>
/// If true, the plant can scream when harvested.
/// </summary>
[DataField]
public bool CanScream = false;
/// <summary>
/// If true, the plant can turn into kudzu.
/// </summary>
[DataField]
public bool TurnIntoKudzu = false;
/// <summary>
/// Which kind of kudzu this plant will turn into if it kuzuifies.
/// </summary>
[DataField]
public EntProtoId KudzuPrototype = "WeakKudzu";
/// <summary>
/// If false, rapidly decrease health while growing. Adds a bit of challenge to keep mutated plants alive via Unviable's frequency.
/// </summary>
[DataField]
public bool Viable = true;
}

View File

@@ -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;
/// <summary>
/// Produce-related data for plant and plant growth cycle.
/// </summary>
[RegisterComponent]
[Access(typeof(BotanySystem))]
public sealed partial class ProduceComponent : SharedProduceComponent
{
[DataField("targetSolution")] public string SolutionName { get; set; } = "food";
/// <summary>
/// Name of the solution container that holds the produce's contents.
/// </summary>
[DataField("targetSolution")]
public string SolutionName { get; set; } = "food";
/// <summary>
/// Seed data used to create a <see cref="SeedComponent"/> when this produce has its seeds extracted.
/// Seed data used to create a <see cref="SeedComponent"/> when this produce has its seeds extracted.
/// </summary>
[DataField]
public SeedData? Seed;
/// <summary>
/// Seed data used to create a <see cref="SeedComponent"/> when this produce has its seeds extracted.
/// Prototype ID for the seed that can be extracted from this produce.
/// </summary>
[DataField(customTypeSerializer: typeof(PrototypeIdSerializer<SeedPrototype>))]
public string? SeedId;
[DataField]
public ProtoId<SeedPrototype>? SeedId;
}

View File

@@ -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;
/// <summary>
/// Data container for plant seed. Contains all info (values for components) for new plant to grow from seed.
/// </summary>
[RegisterComponent, Access(typeof(BotanySystem))]
public sealed partial class SeedComponent : SharedSeedComponent
{
[RegisterComponent, Access(typeof(BotanySystem))]
public sealed partial class SeedComponent : SharedSeedComponent
{
/// <summary>
/// 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 <see
/// cref="SeedId"/>.
/// </summary>
[DataField("seed")]
public SeedData? Seed;
/// <summary>
/// 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 <see
/// cref="SeedId"/>.
/// </summary>
[DataField]
public SeedData? Seed;
/// <summary>
/// If not null, overrides the plant's initial health. Otherwise, the plant's initial health is set to the Endurance value.
/// </summary>
[DataField]
public float? HealthOverride = null;
/// <summary>
/// If not null, overrides the plant's initial health. Otherwise, the plant's initial health is set to the Endurance value.
/// </summary>
[DataField]
public float? HealthOverride = null;
/// <summary>
/// Name of a base seed prototype that is used if <see cref="Seed"/> is null.
/// </summary>
[DataField("seedId", customTypeSerializer: typeof(PrototypeIdSerializer<SeedPrototype>))]
public string? SeedId;
}
/// <summary>
/// Name of a base seed prototype that is used if <see cref="Seed"/> is null.
/// </summary>
[DataField]
public ProtoId<SeedPrototype>? SeedId;
}

View File

@@ -0,0 +1,15 @@
namespace Content.Server.Botany.Components;
/// <summary>
/// Damage tolerance of plant.
/// </summary>
[RegisterComponent]
[DataDefinition]
public sealed partial class UnviableGrowthComponent : Component
{
/// <summary>
/// Amount of damage dealt to the plant per growth tick with unviable.
/// </summary>
[DataField]
public float UnviableDamage = 6f;
}

View File

@@ -0,0 +1,52 @@
namespace Content.Server.Botany.Components;
/// <summary>
/// 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.
/// </summary>
[RegisterComponent]
[DataDefinition]
public sealed partial class WeedPestGrowthComponent : Component
{
/// <summary>
/// Maximum weed level the plant can tolerate before taking damage.
/// </summary>
[DataField]
public float WeedTolerance = 5f;
/// <summary>
/// Maximum pest level the plant can tolerate before taking damage.
/// </summary>
[DataField]
public float PestTolerance = 5f;
/// <summary>
/// Chance per tick for weeds to grow around this plant.
/// </summary>
[DataField]
public float WeedGrowthChance = 0.01f;
/// <summary>
/// Amount of weed growth per successful weed growth tick.
/// </summary>
[DataField]
public float WeedGrowthAmount = 0.5f;
/// <summary>
/// Weed level threshold at which the plant is considered overgrown and will transform into kudzu.
/// </summary>
[DataField]
public float WeedHighLevelThreshold = 10f;
/// <summary>
/// Chance per tick for pests to damage this plant.
/// </summary>
[DataField]
public float PestDamageChance = 0.05f;
/// <summary>
/// Amount of damage dealt to the plant per successful pest damage tick.
/// </summary>
[DataField]
public float PestDamageAmount = 1f;
}

View File

@@ -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
{
/// <summary>
/// Minimum amount of chemical that is added to produce, regardless of the potency
/// </summary>
[DataField("Min")] public FixedPoint2 Min = FixedPoint2.Epsilon;
[DataField] public FixedPoint2 Min = FixedPoint2.Epsilon;
/// <summary>
/// Maximum amount of chemical that can be produced after taking plant potency into account.
/// </summary>
[DataField("Max")] public FixedPoint2 Max;
[DataField] public FixedPoint2 Max;
/// <summary>
/// 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.
/// </summary>
[DataField("PotencyDivisor")] public float PotencyDivisor;
[DataField] public float PotencyDivisor;
/// <summary>
/// Inherent chemical is one that is NOT result of mutation or crossbreeding. These chemicals are removed if species mutation is executed.
/// </summary>
[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
/// <summary>
/// The name of this seed. Determines the name of seed packets.
/// The name of this seed. Determines the name of seed packets.
/// </summary>
[DataField("name")]
[DataField]
public string Name { get; private set; } = "";
/// <summary>
/// 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.
/// </summary>
[DataField("noun")]
[DataField]
public string Noun { get; private set; } = "";
/// <summary>
/// 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.
/// </summary>
[DataField("displayName")]
[DataField]
public string DisplayName { get; private set; } = "";
[DataField("mysterious")] public bool Mysterious;
[DataField] public bool Mysterious;
/// <summary>
/// If true, the properties of this seed cannot be modified.
/// If true, the properties of this seed cannot be modified.
/// </summary>
[DataField("immutable")] public bool Immutable;
[DataField]
public bool Immutable;
/// <summary>
/// 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.
/// </summary>
[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
/// <summary>
/// The entity prototype that is spawned when this type of seed is extracted from produce using a seed extractor.
/// </summary>
[DataField("packetPrototype", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string PacketPrototype = "SeedBase";
/// <summary>
/// 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.
/// </summary>
[DataField]
public List<EntProtoId> ProductPrototypes = new();
[DataField] public Dictionary<string, SeedChemQuantity> Chemicals = new();
[DataField] public Dictionary<Gas, float> ConsumeGasses = new();
[DataField] public Dictionary<Gas, float> 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";
/// <summary>
/// If true, cannot be harvested for seeds. Balances hybrids and
/// mutations.
/// The entity prototypes that are spawned when this type of seed is harvested.
/// </summary>
[DataField] public bool Seedless = false;
[DataField]
public List<EntProtoId> ProductPrototypes = [];
/// <summary>
/// If false, rapidly decrease health while growing. Used to kill off
/// plants with "bad" mutations.
/// </summary>
[DataField] public bool Viable = true;
/// <summary>
/// If true, a sharp tool is required to harvest this plant.
/// </summary>
[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<string, SeedChemQuantity> 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<EntityPrototype>))] public string KudzuPrototype = "WeakKudzu";
[DataField] public bool TurnIntoKudzu;
[DataField] public string? SplatPrototype { get; set; }
#endregion
/// <summary>
/// The mutation effects that have been applied to this plant.
/// </summary>
[DataField] public List<RandomPlantMutation> Mutations { get; set; } = new();
[DataField]
public List<RandomPlantMutation> Mutations { get; set; } = [];
/// <summary>
/// The seed prototypes this seed may mutate into when prompted to.
/// The seed prototypes this seed may mutate into when prompted to.
/// </summary>
[DataField]
public List<ProtoId<SeedPrototype>> MutationPrototypes = new();
public List<ProtoId<SeedPrototype>> MutationPrototypes = [];
/// <summary>
/// Log impact for when the seed is planted.
/// The growth components used by this seed.
/// TODO: Delete after plants transition to entities
/// </summary>
[DataField]
public LogImpact? PlantLogImpact = null;
public GrowthComponentsHolder GrowthComponents = new();
/// <summary>
/// Log impact for when the seed is harvested.
/// Log impact for harvest operations.
/// </summary>
[DataField]
public LogImpact? HarvestLogImpact = null;
public LogImpact? HarvestLogImpact;
/// <summary>
/// Log impact for plant operations.
/// </summary>
[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<ISerializationManager>();
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<EntProtoId>(ProductPrototypes),
MutationPrototypes = new List<ProtoId<SeedPrototype>>(MutationPrototypes),
Chemicals = new Dictionary<string, SeedChemQuantity>(Chemicals),
ConsumeGasses = new Dictionary<Gas, float>(ConsumeGasses),
ExudeGasses = new Dictionary<Gas, float>(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<RandomPlantMutation>(),
Mutations = new List<RandomPlantMutation>(Mutations),
// Newly cloned seed is unique. No need to unnecessarily clone if repeatedly modified.
Unique = true,
@@ -321,8 +190,12 @@ public partial class SeedData
/// </summary>
public SeedData SpeciesChange(SeedData other)
{
var serializationManager = IoCManager.Resolve<ISerializationManager>();
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<ProtoId<SeedPrototype>>(other.MutationPrototypes),
Chemicals = new Dictionary<string, SeedChemQuantity>(Chemicals),
ConsumeGasses = new Dictionary<Gas, float>(ConsumeGasses),
ExudeGasses = new Dictionary<Gas, float>(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,

View File

@@ -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;
/// <summary>
/// Applies atmospheric temperature and pressure effects to plants during growth ticks.
/// Uses current tile gas mixture to penalize or clear warnings based on tolerances.
/// </summary>
public sealed class AtmosphericGrowthSystem : EntitySystem
{
[Dependency] private readonly AtmosphereSystem _atmosphere = default!;
[Dependency] private readonly IRobustRandom _random = default!;
public override void Initialize()
{
SubscribeLocalEvent<AtmosphericGrowthComponent, OnPlantGrowEvent>(OnPlantGrow);
}
private void OnPlantGrow(Entity<AtmosphericGrowthComponent> 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;
}
}
}

View File

@@ -0,0 +1,192 @@
using Content.Server.Botany.Components;
using Content.Shared.Swab;
using Robust.Shared.Random;
namespace Content.Server.Botany.Systems;
/// <summary>
/// Handles baseline plant progression each growth tick: aging, resource consumption,
/// simple viability checks, and basic swab cross-pollination behavior.
/// </summary>
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.
/// <summary>
/// Multiplier for plant growth speed in hydroponics.
/// </summary>
public const float HydroponicsSpeedMultiplier = 1f;
/// <summary>
/// Multiplier for resource consumption (water, nutrients) in hydroponics.
/// </summary>
public const float HydroponicsConsumptionMultiplier = 2f;
public override void Initialize()
{
SubscribeLocalEvent<BasicGrowthComponent, OnPlantGrowEvent>(OnPlantGrow);
SubscribeLocalEvent<BasicGrowthComponent, BotanySwabDoAfterEvent>(OnSwab);
}
private void OnSwab(Entity<BasicGrowthComponent> ent, ref BotanySwabDoAfterEvent args)
{
var component = ent.Comp;
if (args.Cancelled || args.Handled || args.Used == null)
return;
if (!TryComp<BotanySwabComponent>(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<BasicGrowthComponent> ent, ref OnPlantGrowEvent args)
{
var (uid, component) = ent;
if (!TryComp(uid, out PlantHolderComponent? holder)
|| !TryComp<PlantTraitsComponent>(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;
}
}
/// <summary>
/// Affects the growth of a plant by modifying its age or production timing.
/// </summary>
public void AffectGrowth(Entity<PlantHolderComponent> 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;
}
}
}
/// <summary>
/// Event of plant growing ticking.
/// </summary>
[ByRefEvent]
public readonly record struct OnPlantGrowEvent;

View File

@@ -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<BotanySwabComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<BotanySwabComponent, AfterInteractEvent>(OnAfterInteract);
SubscribeLocalEvent<BotanySwabComponent, BotanySwabDoAfterEvent>(OnDoAfter);
@@ -44,7 +45,7 @@ public sealed class BotanySwabSystem : EntitySystem
if (args.Target == null || !args.CanReach || !HasComp<PlantHolderComponent>(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;

View File

@@ -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);

View File

@@ -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<SeedComponent>(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<EntityUid> AutoHarvest(SeedData proto, EntityCoordinates position, int yieldMod = 1)
public IEnumerable<EntityUid> 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<EntityUid>();
return [];
}
public IEnumerable<EntityUid> Harvest(SeedData proto, EntityUid user, int yieldMod = 1)
public IEnumerable<EntityUid> 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<EntityUid>();
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<EntityUid> GenerateProduct(SeedData proto, EntityCoordinates position, int yieldMod = 1)
public IEnumerable<EntityUid> GenerateProduct(SeedData proto, EntityCoordinates position, EntityUid plantEntity)
{
if (!TryGetPlant(proto, out var plant))
return [];
var yieldMod = Comp<PlantHolderComponent>(plantEntity).YieldMod;
var harvest = Comp<PlantHarvestComponent>(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<EntityUid>();
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<ProduceComponent>(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<SharpComponent>(held);
if (!TryGetPlantTraits(proto, out var traits))
return true;
return !traits.Ligneous || traits.Ligneous && held != null && HasComp<SharpComponent>(held);
}
#endregion

View File

@@ -0,0 +1,64 @@
using Content.Server.Atmos.EntitySystems;
using Content.Server.Botany.Components;
using Content.Shared.Atmos;
namespace Content.Server.Botany.Systems;
/// <summary>
/// Consumes and emits configured gases around plants each growth tick, then merges
/// the adjusted gas mixture back into the environment.
/// </summary>
public sealed class ConsumeExudeGasGrowthSystem : EntitySystem
{
[Dependency] private readonly AtmosphereSystem _atmosphere = default!;
public override void Initialize()
{
SubscribeLocalEvent<ConsumeExudeGasGrowthComponent, OnPlantGrowEvent>(OnPlantGrow);
}
private void OnPlantGrow(Entity<ConsumeExudeGasGrowthComponent> 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)));
}
}
}
}

View File

@@ -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<RandomPlantMutationListPrototype> RandomPlantMutations = "RandomPlantMutations";
private static readonly ProtoId<RandomPlantMutationListPrototype> 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
/// <summary>
/// For each random mutation, see if it occurs on this plant this check.
/// </summary>
/// <param name="seed"></param>
/// <param name="severity"></param>
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);
}
/// <summary>
/// Ensures that the plant has all the growth components specified in the seed data.
/// </summary>
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

View File

@@ -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;
/// <summary>
/// Manages harvest readiness and execution for plants, including repeat/self-harvest
/// logic and produce spawning, responding to growth and interaction events.
/// </summary>
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<PlantHarvestComponent, OnPlantGrowEvent>(OnPlantGrow);
SubscribeLocalEvent<PlantHarvestComponent, InteractHandEvent>(OnInteractHand);
SubscribeLocalEvent<PlantHarvestComponent, InteractUsingEvent>(OnInteractUsing);
}
private void OnPlantGrow(Entity<PlantHarvestComponent> 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<PlantHarvestComponent> 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<PlantHarvestComponent> 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<PlantHarvestComponent, PlantHolderComponent> 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<PlantHarvestComponent> 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<PlantHarvestComponent> 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);
}
/// <summary>
/// Auto-harvests a plant.
/// </summary>
public void AutoHarvest(Entity<PlantHarvestComponent, PlantHolderComponent> ent)
{
if (!ent.Comp1.ReadyForHarvest || ent.Comp2.Seed == null)
return;
_botany.AutoHarvest(ent.Comp2.Seed, Transform(ent.Owner).Coordinates, ent);
AfterHarvest(ent);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,43 @@
using Content.Server.Botany.Components;
using Robust.Shared.Random;
namespace Content.Server.Botany.Systems;
/// <summary>
/// Handles plant behavior and growth processing.
/// </summary>
public sealed class PlantSystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _random = default!;
public override void Initialize()
{
SubscribeLocalEvent<PlantComponent, OnPlantGrowEvent>(OnPlantGrow);
}
private void OnPlantGrow(Entity<PlantComponent> 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;
}
}
/// <summary>
/// Adjusts the potency of a plant component.
/// </summary>
public void AdjustPotency(Entity<PlantComponent> ent, float delta)
{
var plant = ent.Comp;
plant.Potency = Math.Max(plant.Potency + delta, 1);
Dirty(ent);
}
}

View File

@@ -0,0 +1,37 @@
using Content.Server.Botany.Components;
namespace Content.Server.Botany.Systems;
/// <summary>
/// Handles toxin accumulation and tolerance for plants, applying health damage
/// and decrementing toxins based on per-tick uptake.
/// </summary>
public sealed class ToxinsSystem : EntitySystem
{
public override void Initialize()
{
SubscribeLocalEvent<PlantToxinsComponent, OnPlantGrowEvent>(OnPlantGrow);
}
private void OnPlantGrow(Entity<PlantToxinsComponent> 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;
}
}

View File

@@ -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);

View File

@@ -0,0 +1,28 @@
using Content.Server.Botany.Components;
namespace Content.Server.Botany.Systems;
/// <summary>
/// Applies a death chance and damage to unviable plants each growth tick, updating visuals when necessary.
/// </summary>
public sealed class UnviableGrowthSystem : EntitySystem
{
public override void Initialize()
{
SubscribeLocalEvent<UnviableGrowthComponent, OnPlantGrowEvent>(OnPlantGrow);
}
private void OnPlantGrow(Entity<UnviableGrowthComponent> 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;
}
}

View File

@@ -0,0 +1,80 @@
using Content.Server.Botany.Components;
using Content.Shared.Coordinates.Helpers;
using Robust.Shared.Random;
namespace Content.Server.Botany.Systems;
/// <summary>
/// Manages weed growth and pest damage per growth tick, and handles tray-level
/// weed spawning and kudzu transformation based on conditions.
/// </summary>
public sealed class WeedPestGrowthSystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _random = default!;
public override void Initialize()
{
SubscribeLocalEvent<WeedPestGrowthComponent, OnPlantGrowEvent>(OnPlantGrow);
SubscribeLocalEvent<PlantHolderComponent, OnPlantGrowEvent>(OnTrayUpdate);
}
private void OnPlantGrow(Entity<WeedPestGrowthComponent> 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;
}
/// <summary>
/// Handles weed growth and kudzu transformation for plant holder trays.
/// </summary>
private void OnTrayUpdate(Entity<PlantHolderComponent> 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;
}
}
}

View File

@@ -5,15 +5,21 @@ using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes;
namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes;
/// <summary>
/// Entity effect that sets plant potency.
/// </summary>
public sealed partial class PlantAdjustPotencyEntityEffectSystem : EntityEffectSystem<PlantHolderComponent, PlantAdjustPotency>
{
[Dependency] private readonly PlantHolderSystem _plantHolder = default!;
[Dependency] private readonly PlantSystem _plant = default!;
protected override void Effect(Entity<PlantHolderComponent> entity, ref EntityEffectEvent<PlantAdjustPotency> 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<PlantComponent>(entity, out var plant))
return;
_plant.AdjustPotency((entity.Owner, plant), args.Effect.Amount);
}
}

View File

@@ -5,15 +5,18 @@ using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes;
namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes;
/// <summary>
/// Entity effect that increments plant age / growth cycle.
/// </summary>
public sealed partial class PlantAffectGrowthEntityEffectSystem : EntityEffectSystem<PlantHolderComponent, PlantAffectGrowth>
{
[Dependency] private readonly PlantHolderSystem _plantHolder = default!;
[Dependency] private readonly BasicGrowthSystem _plantGrowth = default!;
protected override void Effect(Entity<PlantHolderComponent> entity, ref EntityEffectEvent<PlantAffectGrowth> 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);
}
}

View File

@@ -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<Component>(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<float>(currentValue, out var floatVal))
{
MutateFloat(ref floatVal, args.Effect.MinValue, args.Effect.MaxValue, args.Effect.Steps);
field.SetValue(growthComp, floatVal);
return;
}
if (TryGetValue<int>(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<bool>(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<T>(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))
{

View File

@@ -5,6 +5,9 @@ using Robust.Shared.Random;
namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes;
/// <summary>
/// Entity effect that reverts aging of plant.
/// </summary>
public sealed partial class PlantCryoxadoneEntityEffectSystem : EntityEffectSystem<PlantHolderComponent, PlantCryoxadone>
{
[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<PlantComponent>(entity, out var plant) || !TryComp<PlantHarvestComponent>(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;
}
}

View File

@@ -7,6 +7,9 @@ using Content.Shared.Popups;
namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes;
/// <summary>
/// Entity effect that removes ability to get seeds from plant using seed maker.
/// </summary>
public sealed partial class PlantDestroySeedsEntityEffectSystem : EntityEffectSystem<PlantHolderComponent, PlantDestroySeeds>
{
[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<PlantTraitsComponent>(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;
}
}

View File

@@ -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;
/// <summary>
/// Entity effect that enhances plant longevity and endurance.
/// </summary>
public sealed partial class PlantDiethylamineEntityEffectSystem : EntityEffectSystem<PlantHolderComponent, PlantDiethylamine>
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly PlantHolderSystem _plantHolder = default!;
protected override void Effect(Entity<PlantHolderComponent> entity, ref EntityEffectEvent<PlantDiethylamine> 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<PlantComponent>(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++;
}
}

View File

@@ -4,6 +4,9 @@ using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes;
namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes;
/// <summary>
/// Entity effect that mutates plant to lose health with time.
/// </summary>
public sealed partial class PlantPhalanximineEntityEffectSystem : EntityEffectSystem<PlantHolderComponent, PlantPhalanximine>
{
protected override void Effect(Entity<PlantHolderComponent> entity, ref EntityEffectEvent<PlantPhalanximine> 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<PlantTraitsComponent>(entity, out var traits))
traits.Viable = true;
}
}

View File

@@ -6,6 +6,9 @@ using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes;
namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes;
/// <summary>
/// Entity effect that restores ability to get seeds from plant seed maker.
/// </summary>
public sealed partial class PlantRestoreSeedsEntityEffectSystem : EntityEffectSystem<PlantHolderComponent, PlantRestoreSeeds>
{
[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<PlantTraitsComponent>(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;
}
}

View File

@@ -14,28 +14,26 @@ namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes;
public sealed partial class RobustHarvestEntityEffectSystem : EntityEffectSystem<PlantHolderComponent, RobustHarvest>
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly PlantHolderSystem _plantHolder = default!;
protected override void Effect(Entity<PlantHolderComponent> entity, ref EntityEffectEvent<RobustHarvest> 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<PlantComponent>(entity, out var plant) || !TryComp<PlantTraitsComponent>(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--;
}
}
}

View File

@@ -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;
/// <summary>
/// Plant mutation entity effect that forces plant to exude gas while living.
/// </summary>
public sealed partial class PlantMutateExudeGasesEntityEffectSystem : EntityEffectSystem<PlantHolderComponent, PlantMutateExudeGases>
{
[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<ConsumeExudeGasGrowthComponent>(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<Gas>().ToList());
var gas = _random.Pick(Enum.GetValues<Gas>());
if (!gasses.TryAdd(gas, amount))
{
@@ -29,6 +32,9 @@ public sealed partial class PlantMutateExudeGasesEntityEffectSystem : EntityEffe
}
}
/// <summary>
/// Plant mutation entity effect that forces plant to consume gas while living.
/// </summary>
public sealed partial class PlantMutateConsumeGasesEntityEffectSystem : EntityEffectSystem<PlantHolderComponent, PlantMutateConsumeGases>
{
[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<ConsumeExudeGasGrowthComponent>(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<Gas>().ToList());
var gas = _random.Pick(Enum.GetValues<Gas>());
if (!gasses.TryAdd(gas, amount))
{

View File

@@ -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;
/// <summary>
/// Plant mutation entity effect that changes repeatability of plant harvesting (without re-planting).
/// </summary>
public sealed partial class PlantMutateHarvestEntityEffectSystem : EntityEffectSystem<PlantHolderComponent, PlantMutateHarvest>
{
protected override void Effect(Entity<PlantHolderComponent> entity, ref EntityEffectEvent<PlantMutateHarvest> 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<PlantHarvestComponent>(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;
}
}

View File

@@ -1,3 +1,5 @@
using Robust.Shared.Prototypes;
namespace Content.Shared.EntityEffects.Effects.Botany;
/// <summary>
@@ -10,6 +12,15 @@ public sealed partial class PlantMutateConsumeGases : EntityEffectBase<PlantMuta
[DataField]
public float MaxValue = 0.5f;
/// <inheritdoc/>
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<PlantMutateExudeGases>
@@ -19,4 +30,13 @@ public sealed partial class PlantMutateExudeGases : EntityEffectBase<PlantMutate
[DataField]
public float MaxValue = 0.5f;
/// <inheritdoc/>
public override string? EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
{
return Loc.GetString("entity-effect-guidebook-plant-mutate-exude-gasses",
("chance", Probability),
("minValue", MinValue),
("maxValue", MaxValue));
}
}

View File

@@ -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

View File

@@ -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

File diff suppressed because it is too large Load Diff