diff --git a/Content.Server/Botany/Components/PlantHolderComponent.cs b/Content.Server/Botany/Components/PlantHolderComponent.cs
index 809af737ac..8218bead72 100644
--- a/Content.Server/Botany/Components/PlantHolderComponent.cs
+++ b/Content.Server/Botany/Components/PlantHolderComponent.cs
@@ -6,90 +6,90 @@ namespace Content.Server.Botany.Components;
[RegisterComponent]
public sealed partial class PlantHolderComponent : Component
{
- [DataField("nextUpdate", customTypeSerializer: typeof(TimeOffsetSerializer))]
+ [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan NextUpdate = TimeSpan.Zero;
- [ViewVariables(VVAccess.ReadWrite), DataField("updateDelay")]
+ [DataField]
public TimeSpan UpdateDelay = TimeSpan.FromSeconds(3);
- [DataField("lastProduce")]
+ [DataField]
public int LastProduce;
- [ViewVariables(VVAccess.ReadWrite), DataField("missingGas")]
+ [DataField]
public int MissingGas;
- [DataField("cycleDelay")]
+ [DataField]
public TimeSpan CycleDelay = TimeSpan.FromSeconds(15f);
- [DataField("lastCycle", customTypeSerializer: typeof(TimeOffsetSerializer))]
+ [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan LastCycle = TimeSpan.Zero;
- [ViewVariables(VVAccess.ReadWrite), DataField("updateSpriteAfterUpdate")]
+ [DataField]
public bool UpdateSpriteAfterUpdate;
- [ViewVariables(VVAccess.ReadWrite), DataField("drawWarnings")]
+ [DataField]
public bool DrawWarnings = false;
- [ViewVariables(VVAccess.ReadWrite), DataField("waterLevel")]
+ [DataField]
public float WaterLevel = 100f;
- [ViewVariables(VVAccess.ReadWrite), DataField("nutritionLevel")]
+ [DataField]
public float NutritionLevel = 100f;
- [ViewVariables(VVAccess.ReadWrite), DataField("pestLevel")]
+ [DataField]
public float PestLevel;
- [ViewVariables(VVAccess.ReadWrite), DataField("weedLevel")]
+ [DataField]
public float WeedLevel;
- [ViewVariables(VVAccess.ReadWrite), DataField("toxins")]
+ [DataField]
public float Toxins;
- [ViewVariables(VVAccess.ReadWrite), DataField("age")]
+ [DataField]
public int Age;
- [ViewVariables(VVAccess.ReadWrite), DataField("skipAging")]
+ [DataField]
public int SkipAging;
- [ViewVariables(VVAccess.ReadWrite), DataField("dead")]
+ [DataField]
public bool Dead;
- [ViewVariables(VVAccess.ReadWrite), DataField("harvest")]
+ [DataField]
public bool Harvest;
- [ViewVariables(VVAccess.ReadWrite), DataField("sampled")]
+ [DataField]
public bool Sampled;
- [ViewVariables(VVAccess.ReadWrite), DataField("yieldMod")]
+ [DataField]
public int YieldMod = 1;
- [ViewVariables(VVAccess.ReadWrite), DataField("mutationMod")]
+ [DataField]
public float MutationMod = 1f;
- [ViewVariables(VVAccess.ReadWrite), DataField("mutationLevel")]
+ [DataField]
public float MutationLevel;
- [ViewVariables(VVAccess.ReadWrite), DataField("health")]
+ [DataField]
public float Health;
- [ViewVariables(VVAccess.ReadWrite), DataField("weedCoefficient")]
+ [DataField]
public float WeedCoefficient = 1f;
- [ViewVariables(VVAccess.ReadWrite), DataField("seed")]
+ [DataField]
public SeedData? Seed;
- [ViewVariables(VVAccess.ReadWrite), DataField("improperHeat")]
+ [DataField]
public bool ImproperHeat;
- [ViewVariables(VVAccess.ReadWrite), DataField("improperPressure")]
+ [DataField]
public bool ImproperPressure;
- [ViewVariables(VVAccess.ReadWrite), DataField("improperLight")]
+ [DataField]
public bool ImproperLight;
- [ViewVariables(VVAccess.ReadWrite), DataField("forceUpdate")]
+ [DataField]
public bool ForceUpdate;
- [ViewVariables(VVAccess.ReadWrite), DataField("solution")]
+ [DataField]
public string SoilSolutionName = "soil";
[DataField]
diff --git a/Content.Server/Botany/Components/ProduceComponent.cs b/Content.Server/Botany/Components/ProduceComponent.cs
index b3c4e1c95a..db4ed62dd3 100644
--- a/Content.Server/Botany/Components/ProduceComponent.cs
+++ b/Content.Server/Botany/Components/ProduceComponent.cs
@@ -13,12 +13,12 @@ public sealed partial class ProduceComponent : SharedProduceComponent
///
/// Seed data used to create a when this produce has its seeds extracted.
///
- [DataField("seed")]
+ [DataField]
public SeedData? Seed;
///
/// Seed data used to create a when this produce has its seeds extracted.
///
- [DataField("seedId", customTypeSerializer: typeof(PrototypeIdSerializer))]
+ [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))]
public string? SeedId;
}
diff --git a/Content.Server/Botany/SeedPrototype.cs b/Content.Server/Botany/SeedPrototype.cs
index 39f06a6436..7a3e08883d 100644
--- a/Content.Server/Botany/SeedPrototype.cs
+++ b/Content.Server/Botany/SeedPrototype.cs
@@ -2,6 +2,7 @@ using Content.Server.Botany.Components;
using Content.Server.Botany.Systems;
using Content.Shared.Atmos;
using Content.Shared.EntityEffects;
+using Content.Shared.Random;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
@@ -132,78 +133,67 @@ public partial class SeedData
[DataField("productPrototypes", customTypeSerializer: typeof(PrototypeIdListSerializer))]
public List ProductPrototypes = new();
- [DataField("chemicals")] public Dictionary Chemicals = new();
+ [DataField] public Dictionary Chemicals = new();
- [DataField("consumeGasses")] public Dictionary ConsumeGasses = new();
+ [DataField] public Dictionary ConsumeGasses = new();
- [DataField("exudeGasses")] public Dictionary ExudeGasses = new();
+ [DataField] public Dictionary ExudeGasses = new();
#endregion
#region Tolerances
- [DataField("nutrientConsumption")] public float NutrientConsumption = 0.75f;
+ [DataField] public float NutrientConsumption = 0.75f;
- [DataField("waterConsumption")] public float WaterConsumption = 0.5f;
- [DataField("idealHeat")] public float IdealHeat = 293f;
- [DataField("heatTolerance")] public float HeatTolerance = 10f;
- [DataField("idealLight")] public float IdealLight = 7f;
- [DataField("lightTolerance")] public float LightTolerance = 3f;
- [DataField("toxinsTolerance")] public float ToxinsTolerance = 4f;
+ [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("lowPressureTolerance")] public float LowPressureTolerance = 81f;
+ [DataField] public float LowPressureTolerance = 81f;
- [DataField("highPressureTolerance")] public float HighPressureTolerance = 121f;
+ [DataField] public float HighPressureTolerance = 121f;
- [DataField("pestTolerance")] public float PestTolerance = 5f;
+ [DataField] public float PestTolerance = 5f;
- [DataField("weedTolerance")] public float WeedTolerance = 5f;
+ [DataField] public float WeedTolerance = 5f;
- [DataField("weedHighLevelThreshold")] public float WeedHighLevelThreshold = 10f;
+ [DataField] public float WeedHighLevelThreshold = 10f;
#endregion
#region General traits
- [DataField("endurance")] public float Endurance = 100f;
+ [DataField] public float Endurance = 100f;
- [DataField("yield")] public int Yield;
- [DataField("lifespan")] public float Lifespan;
- [DataField("maturation")] public float Maturation;
- [DataField("production")] public float Production;
- [DataField("growthStages")] public int GrowthStages = 6;
+ [DataField] public int Yield;
+ [DataField] public float Lifespan;
+ [DataField] public float Maturation;
+ [DataField] public float Production;
+ [DataField] public int GrowthStages = 6;
- [ViewVariables(VVAccess.ReadWrite)]
- [DataField("harvestRepeat")] public HarvestType HarvestRepeat = HarvestType.NoRepeat;
+ [DataField] public HarvestType HarvestRepeat = HarvestType.NoRepeat;
- [DataField("potency")] public float Potency = 1f;
+ [DataField] public float Potency = 1f;
///
/// If true, cannot be harvested for seeds. Balances hybrids and
/// mutations.
///
- [DataField("seedless")] public bool Seedless = false;
+ [DataField] public bool Seedless = false;
///
/// If false, rapidly decrease health while growing. Used to kill off
/// plants with "bad" mutations.
///
- [DataField("viable")] public bool Viable = true;
-
- ///
- /// If true, fruit slips players.
- ///
- [DataField("slip")] public bool Slip = false;
-
- ///
- /// If true, fruits are sentient.
- ///
- [DataField("sentient")] public bool Sentient = false;
+ [DataField] public bool Viable = true;
///
/// If true, a sharp tool is required to harvest this plant.
///
- [DataField("ligneous")] public bool Ligneous;
+ [DataField] public bool Ligneous;
// No, I'm not removing these.
// if you re-add these, make sure that they get cloned.
@@ -222,36 +212,35 @@ public partial class SeedData
#region Cosmetics
- [DataField("plantRsi", required: true)]
+ [DataField(required: true)]
public ResPath PlantRsi { get; set; } = default!;
- [DataField("plantIconState")] public string PlantIconState { get; set; } = "produce";
+ [DataField] public string PlantIconState { get; set; } = "produce";
///
- /// Screams random sound, could be strict sound SoundPathSpecifier or collection SoundCollectionSpecifier
- /// base class is SoundSpecifier
+ /// Screams random sound from collection SoundCollectionSpecifier
///
- [DataField("screamSound")]
+ [DataField]
public SoundSpecifier ScreamSound = new SoundCollectionSpecifier("PlantScreams", AudioParams.Default.WithVolume(-10));
[DataField("screaming")] public bool CanScream;
- [DataField("bioluminescent")] public bool Bioluminescent;
- [DataField("bioluminescentColor")] public Color BioluminescentColor { get; set; } = Color.White;
+ [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))] public string KudzuPrototype = "WeakKudzu";
- public float BioluminescentRadius = 2f;
-
- [DataField("kudzuPrototype", customTypeSerializer: typeof(PrototypeIdSerializer))] public string KudzuPrototype = "WeakKudzu";
-
- [DataField("turnIntoKudzu")] public bool TurnIntoKudzu;
- [DataField("splatPrototype")] public string? SplatPrototype { get; set; }
+ [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();
+
///
/// The seed prototypes this seed may mutate into when prompted to.
///
- [DataField("mutationPrototypes", customTypeSerializer: typeof(PrototypeIdListSerializer))]
+ [DataField(customTypeSerializer: typeof(PrototypeIdListSerializer))]
public List MutationPrototypes = new();
public SeedData Clone()
@@ -295,17 +284,14 @@ public partial class SeedData
Seedless = Seedless,
Viable = Viable,
- Slip = Slip,
- Sentient = Sentient,
Ligneous = Ligneous,
PlantRsi = PlantRsi,
PlantIconState = PlantIconState,
- Bioluminescent = Bioluminescent,
CanScream = CanScream,
TurnIntoKudzu = TurnIntoKudzu,
- BioluminescentColor = BioluminescentColor,
SplatPrototype = SplatPrototype,
+ Mutations = Mutations,
// Newly cloned seed is unique. No need to unnecessarily clone if repeatedly modified.
Unique = true,
@@ -356,18 +342,16 @@ public partial class SeedData
HarvestRepeat = HarvestRepeat,
Potency = Potency,
+ Mutations = Mutations,
+
Seedless = Seedless,
Viable = Viable,
- Slip = Slip,
- Sentient = Sentient,
Ligneous = Ligneous,
PlantRsi = other.PlantRsi,
PlantIconState = other.PlantIconState,
- Bioluminescent = Bioluminescent,
CanScream = CanScream,
TurnIntoKudzu = TurnIntoKudzu,
- BioluminescentColor = BioluminescentColor,
SplatPrototype = other.SplatPrototype,
// Newly cloned seed is unique. No need to unnecessarily clone if repeatedly modified.
diff --git a/Content.Server/Botany/Systems/BotanySystem.Produce.cs b/Content.Server/Botany/Systems/BotanySystem.Produce.cs
index 34559a8304..8fdf96f57b 100644
--- a/Content.Server/Botany/Systems/BotanySystem.Produce.cs
+++ b/Content.Server/Botany/Systems/BotanySystem.Produce.cs
@@ -1,4 +1,5 @@
using Content.Server.Botany.Components;
+using Content.Shared.EntityEffects;
using Content.Shared.FixedPoint;
namespace Content.Server.Botany.Systems;
@@ -10,6 +11,15 @@ public sealed partial class BotanySystem
if (!TryGetSeed(produce, out var seed))
return;
+ foreach (var mutation in seed.Mutations)
+ {
+ if (mutation.AppliesToProduce)
+ {
+ var args = new EntityEffectBaseArgs(uid, EntityManager);
+ mutation.Effect.Effect(args);
+ }
+ }
+
if (!_solutionContainerSystem.EnsureSolution(uid,
produce.SolutionName,
out var solutionContainer,
diff --git a/Content.Server/Botany/Systems/BotanySystem.Seed.cs b/Content.Server/Botany/Systems/BotanySystem.Seed.cs
index c988e5338c..1487ed71d4 100644
--- a/Content.Server/Botany/Systems/BotanySystem.Seed.cs
+++ b/Content.Server/Botany/Systems/BotanySystem.Seed.cs
@@ -5,16 +5,11 @@ using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Botany;
using Content.Shared.Examine;
using Content.Shared.Hands.EntitySystems;
-using Content.Shared.Physics;
using Content.Shared.Popups;
using Content.Shared.Random;
using Content.Shared.Random.Helpers;
-using Content.Shared.Slippery;
-using Content.Shared.StepTrigger.Components;
using Robust.Server.GameObjects;
using Robust.Shared.Map;
-using Robust.Shared.Physics;
-using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
@@ -34,7 +29,6 @@ public sealed partial class BotanySystem : EntitySystem
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly FixtureSystem _fixtureSystem = default!;
- [Dependency] private readonly CollisionWakeSystem _colWakeSystem = default!;
[Dependency] private readonly RandomHelperSystem _randomHelper = default!;
public override void Initialize()
@@ -183,30 +177,6 @@ public sealed partial class BotanySystem : EntitySystem
_metaData.SetEntityDescription(entity,
metaData.EntityDescription + " " + Loc.GetString("botany-mysterious-description-addon"), metaData);
}
-
- if (proto.Bioluminescent)
- {
- var light = _light.EnsureLight(entity);
- _light.SetRadius(entity, proto.BioluminescentRadius, light);
- _light.SetColor(entity, proto.BioluminescentColor, light);
- // TODO: Ayo why you copy-pasting code between here and plantholder?
- _light.SetCastShadows(entity, false, light); // this is expensive, and botanists make lots of plants
- }
-
- if (proto.Slip)
- {
- var slippery = EnsureComp(entity);
- Dirty(entity, slippery);
- EnsureComp(entity);
- // Need a fixture with a slip layer in order to actually do the slipping
- var fixtures = EnsureComp(entity);
- var body = EnsureComp(entity);
- var shape = fixtures.Fixtures["fix1"].Shape;
- _fixtureSystem.TryCreateFixture(entity, shape, "slips", 1, false, (int) CollisionGroup.SlipLayer, manager: fixtures, body: body);
- // Need to disable collision wake so that mobs can collide with and slip on it
- var collisionWake = EnsureComp(entity);
- _colWakeSystem.SetEnabled(entity, false, collisionWake);
- }
}
return products;
diff --git a/Content.Server/Botany/Systems/MutationSystem.cs b/Content.Server/Botany/Systems/MutationSystem.cs
index d3159655f5..07a24d19f6 100644
--- a/Content.Server/Botany/Systems/MutationSystem.cs
+++ b/Content.Server/Botany/Systems/MutationSystem.cs
@@ -1,9 +1,9 @@
+using Content.Shared.Atmos;
+using Content.Shared.EntityEffects;
+using Content.Shared.Random;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
-using Content.Shared.Random;
-using Content.Shared.Random.Helpers;
using System.Linq;
-using Content.Shared.Atmos;
namespace Content.Server.Botany;
@@ -11,25 +11,40 @@ public sealed class MutationSystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _robustRandom = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
- private WeightedRandomFillSolutionPrototype _randomChems = default!;
-
+ private RandomPlantMutationListPrototype _randomMutations = default!;
public override void Initialize()
{
- _randomChems = _prototypeManager.Index("RandomPickBotanyReagent");
+ _randomMutations = _prototypeManager.Index("RandomPlantMutations");
}
///
- /// Main idea: Simulate genetic mutation using random binary flips. Each
- /// seed attribute can be encoded with a variable number of bits, e.g.
- /// NutrientConsumption is represented by 5 bits randomly distributed in the
- /// plant's genome which thermometer code the floating value between 0.1 and
- /// 5. 1 unit of mutation flips one bit in the plant's genome, which changes
- /// NutrientConsumption if one of those 5 bits gets affected.
- ///
- /// You MUST clone() seed before mutating it!
+ /// For each random mutation, see if it occurs on this plant this check.
///
- public void MutateSeed(ref SeedData seed, float severity)
+ ///
+ ///
+ public void CheckRandomMutations(EntityUid plantHolder, ref SeedData seed, float severity)
+ {
+ foreach (var mutation in _randomMutations.mutations)
+ {
+ if (Random(mutation.BaseOdds * severity))
+ {
+ if (mutation.AppliesToPlant)
+ {
+ var args = new EntityEffectBaseArgs(plantHolder, EntityManager);
+ mutation.Effect.Effect(args);
+ }
+ // Stat adjustments do not persist by being an attached effect, they just change the stat.
+ if (mutation.Persists && !seed.Mutations.Any(m => m.Name == mutation.Name))
+ seed.Mutations.Add(mutation);
+ }
+ }
+ }
+
+ ///
+ /// Checks all defined mutations against a seed to see which of them are applied.
+ ///
+ public void MutateSeed(EntityUid plantHolder, ref SeedData seed, float severity)
{
if (!seed.Unique)
{
@@ -37,57 +52,7 @@ public sealed class MutationSystem : EntitySystem
return;
}
- // Add up everything in the bits column and put the number here.
- const int totalbits = 262;
-
- #pragma warning disable IDE0055 // disable formatting warnings because this looks more readable
- // Tolerances (55)
- MutateFloat(ref seed.NutrientConsumption , 0.05f, 1.2f, 5, totalbits, severity);
- MutateFloat(ref seed.WaterConsumption , 3f , 9f , 5, totalbits, severity);
- MutateFloat(ref seed.IdealHeat , 263f , 323f, 5, totalbits, severity);
- MutateFloat(ref seed.HeatTolerance , 2f , 25f , 5, totalbits, severity);
- MutateFloat(ref seed.IdealLight , 0f , 14f , 5, totalbits, severity);
- MutateFloat(ref seed.LightTolerance , 1f , 5f , 5, totalbits, severity);
- MutateFloat(ref seed.ToxinsTolerance , 1f , 10f , 5, totalbits, severity);
- MutateFloat(ref seed.LowPressureTolerance , 60f , 100f, 5, totalbits, severity);
- MutateFloat(ref seed.HighPressureTolerance, 100f , 140f, 5, totalbits, severity);
- MutateFloat(ref seed.PestTolerance , 0f , 15f , 5, totalbits, severity);
- MutateFloat(ref seed.WeedTolerance , 0f , 15f , 5, totalbits, severity);
-
- // Stats (30*2 = 60)
- MutateFloat(ref seed.Endurance , 50f , 150f, 5, totalbits, 2 * severity);
- MutateInt(ref seed.Yield , 3 , 10 , 5, totalbits, 2 * severity);
- MutateFloat(ref seed.Lifespan , 10f , 80f , 5, totalbits, 2 * severity);
- MutateFloat(ref seed.Maturation , 3f , 8f , 5, totalbits, 2 * severity);
- MutateFloat(ref seed.Production , 1f , 10f , 5, totalbits, 2 * severity);
- MutateFloat(ref seed.Potency , 30f , 100f, 5, totalbits, 2 * severity);
-
- // Kill the plant (30)
- MutateBool(ref seed.Viable , false, 30, totalbits, severity);
-
- // Fun (72)
- MutateBool(ref seed.Seedless , true , 10, totalbits, severity);
- MutateBool(ref seed.Slip , true , 10, totalbits, severity);
- MutateBool(ref seed.Sentient , true , 2 , totalbits, severity);
- MutateBool(ref seed.Ligneous , true , 10, totalbits, severity);
- MutateBool(ref seed.Bioluminescent, true , 10, totalbits, severity);
- MutateBool(ref seed.TurnIntoKudzu , true , 10, totalbits, severity);
- MutateBool(ref seed.CanScream , true , 10, totalbits, severity);
- seed.BioluminescentColor = RandomColor(seed.BioluminescentColor, 10, totalbits, severity);
- #pragma warning restore IDE0055
-
- // ConstantUpgade (10)
- MutateHarvestType(ref seed.HarvestRepeat, 10, totalbits, severity);
-
- // Gas (5)
- MutateGasses(ref seed.ExudeGasses, 0.01f, 0.5f, 4, totalbits, severity);
- MutateGasses(ref seed.ConsumeGasses, 0.01f, 0.5f, 1, totalbits, severity);
-
- // Chems (20)
- MutateChemicals(ref seed.Chemicals, 20, totalbits, severity);
-
- // Species (10)
- MutateSpecies(ref seed, 10, totalbits, severity);
+ CheckRandomMutations(plantHolder, ref seed, severity);
}
public SeedData Cross(SeedData a, SeedData b)
@@ -115,19 +80,18 @@ public sealed class MutationSystem : EntitySystem
CrossFloat(ref result.Production, a.Production);
CrossFloat(ref result.Potency, a.Potency);
- // we do not transfer Sentient to another plant to avoid ghost role spam
CrossBool(ref result.Seedless, a.Seedless);
- CrossBool(ref result.Viable, a.Viable);
- CrossBool(ref result.Slip, a.Slip);
CrossBool(ref result.Ligneous, a.Ligneous);
- CrossBool(ref result.Bioluminescent, a.Bioluminescent);
CrossBool(ref result.TurnIntoKudzu, a.TurnIntoKudzu);
CrossBool(ref result.CanScream, a.CanScream);
CrossGasses(ref result.ExudeGasses, a.ExudeGasses);
CrossGasses(ref result.ConsumeGasses, a.ConsumeGasses);
- result.BioluminescentColor = Random(0.5f) ? a.BioluminescentColor : result.BioluminescentColor;
+ // LINQ Explanation
+ // For the list of mutation effects on both plants, use a 50% chance to pick each one.
+ // Union all of the chosen mutations into one list, and pick ones with a Distinct (unique) name.
+ result.Mutations = result.Mutations.Where(m => Random(0.5f)).Union(a.Mutations.Where(m => Random(0.5f))).DistinctBy(m => m.Name).ToList();
// Hybrids have a high chance of being seedless. Balances very
// effective hybrid crossings.
@@ -139,206 +103,6 @@ public sealed class MutationSystem : EntitySystem
return result;
}
- // Mutate reference 'val' between 'min' and 'max' by pretending the value
- // is representable by a thermometer code with 'bits' number of bits and
- // randomly flipping some of them.
- //
- // 'totalbits' and 'mult' are used only to calculate the probability that
- // one bit gets flipped.
- private void MutateFloat(ref float val, float min, float max, int bits, int totalbits, float mult)
- {
- // Probability that a bit flip happens for this value's representation in thermometer code.
- float probBitflip = mult * bits / totalbits;
- probBitflip = Math.Clamp(probBitflip, 0, 1);
- if (!Random(probBitflip))
- return;
-
- if (min == max)
- {
- val = min;
- return;
- }
-
- // 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);
- // 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;
- int valIntMutated;
- if (Random(probIncrease))
- {
- valIntMutated = valInt + 1;
- }
- else
- {
- valIntMutated = valInt - 1;
- }
-
- // Set value based on mutated thermometer code.
- float valMutated = Math.Clamp((float)valIntMutated / bits * (max - min) + min, min, max);
- val = valMutated;
- }
-
- private void MutateInt(ref int val, int min, int max, int bits, int totalbits, float mult)
- {
- // Probability that a bit flip happens for this value's representation in thermometer code.
- float probBitflip = mult * bits / totalbits;
- probBitflip = Math.Clamp(probBitflip, 0, 1);
- if (!Random(probBitflip))
- return;
-
- if (min == max)
- {
- val = min;
- return;
- }
-
- // 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);
- // 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;
- int valMutated;
- if (Random(probIncrease))
- {
- valMutated = val + 1;
- }
- else
- {
- valMutated = val - 1;
- }
-
- valMutated = Math.Clamp(valMutated, min, max);
- val = valMutated;
- }
-
- private void MutateBool(ref bool val, bool polarity, int bits, int totalbits, float mult)
- {
- // Probability that a bit flip happens for this value.
- float probSet = mult * bits / totalbits;
- probSet = Math.Clamp(probSet, 0, 1);
- if (!Random(probSet))
- return;
-
- val = polarity;
- }
-
- private void MutateHarvestType(ref HarvestType val, int bits, int totalbits, float mult)
- {
- float probModify = mult * bits / totalbits;
- probModify = Math.Clamp(probModify, 0, 1);
-
- if (!Random(probModify))
- return;
-
- if (val == HarvestType.NoRepeat)
- val = HarvestType.Repeat;
- else if (val == HarvestType.Repeat)
- val = HarvestType.SelfHarvest;
- }
-
- private void MutateGasses(ref Dictionary gasses, float min, float max, int bits, int totalbits, float mult)
- {
- float probModify = mult * bits / totalbits;
- probModify = Math.Clamp(probModify, 0, 1);
- if (!Random(probModify))
- return;
-
- // Add a random amount of a random gas to this gas dictionary
- float amount = _robustRandom.NextFloat(min, max);
- Gas gas = _robustRandom.Pick(Enum.GetValues(typeof(Gas)).Cast().ToList());
- if (gasses.ContainsKey(gas))
- {
- gasses[gas] += amount;
- }
- else
- {
- gasses.Add(gas, amount);
- }
- }
-
- private void MutateChemicals(ref Dictionary chemicals, int bits, int totalbits, float mult)
- {
- float probModify = mult * bits / totalbits;
- probModify = Math.Clamp(probModify, 0, 1);
- if (!Random(probModify))
- return;
-
- // Add a random amount of a random chemical to this set of chemicals
- if (_randomChems != null)
- {
- var pick = _randomChems.Pick(_robustRandom);
- string chemicalId = pick.reagent;
- int amount = _robustRandom.Next(1, (int)pick.quantity);
- SeedChemQuantity seedChemQuantity = new SeedChemQuantity();
- if (chemicals.ContainsKey(chemicalId))
- {
- seedChemQuantity.Min = chemicals[chemicalId].Min;
- seedChemQuantity.Max = chemicals[chemicalId].Max + amount;
- }
- else
- {
- seedChemQuantity.Min = 1;
- seedChemQuantity.Max = 1 + amount;
- seedChemQuantity.Inherent = false;
- }
- int potencyDivisor = (int)Math.Ceiling(100.0f / seedChemQuantity.Max);
- seedChemQuantity.PotencyDivisor = potencyDivisor;
- chemicals[chemicalId] = seedChemQuantity;
- }
- }
-
- private void MutateSpecies(ref SeedData seed, int bits, int totalbits, float mult)
- {
- float p = mult * bits / totalbits;
- p = Math.Clamp(p, 0, 1);
- if (!Random(p))
- return;
-
- if (seed.MutationPrototypes.Count == 0)
- return;
-
- var targetProto = _robustRandom.Pick(seed.MutationPrototypes);
- _prototypeManager.TryIndex(targetProto, out SeedPrototype? protoSeed);
-
- if (protoSeed == null)
- {
- Log.Error($"Seed prototype could not be found: {targetProto}!");
- return;
- }
-
- seed = seed.SpeciesChange(protoSeed);
- }
-
- private Color RandomColor(Color color, int bits, int totalbits, float mult)
- {
- float probModify = mult * bits / totalbits;
- if (Random(probModify))
- {
- var colors = new List{
- Color.White,
- Color.Red,
- Color.Yellow,
- Color.Green,
- Color.Blue,
- Color.Purple,
- Color.Pink
- };
- return _robustRandom.Pick(colors);
- }
- return color;
- }
-
private void CrossChemicals(ref Dictionary val, Dictionary other)
{
// Go through chemicals from the pollen in swab
diff --git a/Content.Server/Botany/Systems/PlantHolderSystem.cs b/Content.Server/Botany/Systems/PlantHolderSystem.cs
index 002a054339..0fdca029b7 100644
--- a/Content.Server/Botany/Systems/PlantHolderSystem.cs
+++ b/Content.Server/Botany/Systems/PlantHolderSystem.cs
@@ -1,8 +1,6 @@
-using Content.Server.Atmos;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Botany.Components;
using Content.Server.Fluids.Components;
-using Content.Server.Ghost.Roles.Components;
using Content.Server.Kitchen.Components;
using Content.Server.Popups;
using Content.Shared.Chemistry.EntitySystems;
@@ -79,7 +77,7 @@ 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));
+ var result = Math.Max(1, (int)(component.Age * component.Seed.GrowthStages / component.Seed.Maturation));
return result;
}
@@ -125,9 +123,9 @@ public sealed class PlantHolderSystem : EntitySystem
args.PushMarkup(Loc.GetString("plant-holder-component-pest-high-level-message"));
args.PushMarkup(Loc.GetString($"plant-holder-component-water-level-message",
- ("waterLevel", (int) component.WaterLevel)));
+ ("waterLevel", (int)component.WaterLevel)));
args.PushMarkup(Loc.GetString($"plant-holder-component-nutrient-level-message",
- ("nutritionLevel", (int) component.NutritionLevel)));
+ ("nutritionLevel", (int)component.NutritionLevel)));
if (component.DrawWarnings)
{
@@ -299,21 +297,12 @@ public sealed class PlantHolderSystem : EntitySystem
healthOverride = component.Health;
}
var packetSeed = component.Seed;
- if (packetSeed.Sentient)
- {
- packetSeed = packetSeed.Clone(); // clone before modifying the seed
- packetSeed.Sentient = false;
- }
- else
- {
- packetSeed.Unique = false;
- }
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);
if (_random.Prob(0.3f))
@@ -459,7 +448,7 @@ public sealed class PlantHolderSystem : EntitySystem
else
{
if (_random.Prob(0.8f))
- component.Age += (int) (1 * HydroponicsSpeedMultiplier);
+ component.Age += (int)(1 * HydroponicsSpeedMultiplier);
component.UpdateSpriteAfterUpdate = true;
}
@@ -632,12 +621,6 @@ public sealed class PlantHolderSystem : EntitySystem
else if (component.Age < 0) // Revert back to seed packet!
{
var packetSeed = component.Seed;
- if (packetSeed.Sentient)
- {
- if (!packetSeed.Unique) // clone if necessary before modifying the seed
- packetSeed = packetSeed.Clone();
- packetSeed.Sentient = false; // remove Sentient to avoid ghost role spam
- }
// 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);
@@ -674,14 +657,6 @@ public sealed class PlantHolderSystem : EntitySystem
CheckLevelSanity(uid, component);
- if (component.Seed.Sentient)
- {
- var ghostRole = EnsureComp(uid);
- EnsureComp(uid);
- ghostRole.RoleName = MetaData(uid).EntityName;
- ghostRole.RoleDescription = Loc.GetString("station-event-random-sentience-role-description", ("name", ghostRole.RoleName));
- }
-
if (component.UpdateSpriteAfterUpdate)
UpdateSprite(uid, component);
}
@@ -911,7 +886,7 @@ public sealed class PlantHolderSystem : EntitySystem
if (component.Seed != null)
{
EnsureUniqueSeed(uid, component);
- _mutation.MutateSeed(ref component.Seed, severity);
+ _mutation.MutateSeed(uid, ref component.Seed, severity);
}
}
@@ -922,19 +897,6 @@ public sealed class PlantHolderSystem : EntitySystem
component.UpdateSpriteAfterUpdate = false;
- if (component.Seed != null && component.Seed.Bioluminescent)
- {
- var light = EnsureComp(uid);
- _pointLight.SetRadius(uid, component.Seed.BioluminescentRadius, light);
- _pointLight.SetColor(uid, component.Seed.BioluminescentColor, light);
- _pointLight.SetCastShadows(uid, false, light);
- Dirty(uid, light);
- }
- else
- {
- RemComp(uid);
- }
-
if (!TryComp(uid, out var app))
return;
diff --git a/Content.Server/Botany/Systems/SeedExtractorSystem.cs b/Content.Server/Botany/Systems/SeedExtractorSystem.cs
index 4a0d56bfe9..93f76473ff 100644
--- a/Content.Server/Botany/Systems/SeedExtractorSystem.cs
+++ b/Content.Server/Botany/Systems/SeedExtractorSystem.cs
@@ -43,12 +43,6 @@ public sealed class SeedExtractorSystem : EntitySystem
var coords = Transform(uid).Coordinates;
var packetSeed = seed;
- if (packetSeed.Sentient)
- {
- if (!packetSeed.Unique) // clone if necessary before modifying the seed
- packetSeed = packetSeed.Clone();
- packetSeed.Sentient = false; // remove Sentient to avoid ghost role spam
- }
if (amount > 1)
packetSeed.Unique = false;
diff --git a/Content.Server/EntityEffects/Effects/Glow.cs b/Content.Server/EntityEffects/Effects/Glow.cs
new file mode 100644
index 0000000000..9f03476729
--- /dev/null
+++ b/Content.Server/EntityEffects/Effects/Glow.cs
@@ -0,0 +1,48 @@
+using Content.Shared.EntityEffects;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+
+namespace Content.Server.EntityEffects.Effects;
+
+///
+/// Makes a mob glow.
+///
+public sealed partial class Glow : EntityEffect
+{
+ [DataField]
+ public float Radius = 2f;
+
+ [DataField]
+ public Color Color = Color.Black;
+
+ private static readonly List Colors = new()
+ {
+ Color.White,
+ Color.Red,
+ Color.Yellow,
+ Color.Green,
+ Color.Blue,
+ Color.Purple,
+ Color.Pink
+ };
+
+ public override void Effect(EntityEffectBaseArgs args)
+ {
+ if (Color == Color.Black)
+ {
+ var random = IoCManager.Resolve();
+ Color = random.Pick(Colors);
+ }
+
+ var lightSystem = args.EntityManager.System();
+ var light = lightSystem.EnsureLight(args.TargetEntity);
+ lightSystem.SetRadius(args.TargetEntity, Radius, light);
+ lightSystem.SetColor(args.TargetEntity, Color, light);
+ lightSystem.SetCastShadows(args.TargetEntity, false, light); // this is expensive, and botanists make lots of plants
+ }
+
+ protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
+ {
+ return "TODO";
+ }
+}
diff --git a/Content.Server/EntityEffects/Effects/PlantChangeStat.cs b/Content.Server/EntityEffects/Effects/PlantChangeStat.cs
new file mode 100644
index 0000000000..9592ff779d
--- /dev/null
+++ b/Content.Server/EntityEffects/Effects/PlantChangeStat.cs
@@ -0,0 +1,142 @@
+using Content.Server.Botany;
+using Content.Server.Botany.Components;
+using Content.Shared.EntityEffects;
+using JetBrains.Annotations;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+
+namespace Content.Server.EntityEffects.Effects.PlantMetabolism;
+
+[UsedImplicitly]
+public sealed partial class PlantChangeStat : EntityEffect
+{
+ [DataField]
+ public string TargetValue;
+
+ [DataField]
+ public float MinValue;
+
+ [DataField]
+ public float MaxValue;
+
+ [DataField]
+ public int Steps;
+
+ public override void Effect(EntityEffectBaseArgs args)
+ {
+ var plantHolder = args.EntityManager.GetComponent(args.TargetEntity);
+ if (plantHolder == null || plantHolder.Seed == null)
+ return;
+
+ var member = plantHolder.Seed.GetType().GetField(TargetValue);
+ var mutationSys = args.EntityManager.System();
+
+ if (member == null)
+ {
+ mutationSys.Log.Error(this.GetType().Name + " Error: Member " + TargetValue + " not found on " + plantHolder.GetType().Name + ". Did you misspell it?");
+ return;
+ }
+
+ var currentValObj = member.GetValue(plantHolder.Seed);
+ if (currentValObj == null)
+ return;
+
+ if (member.FieldType == typeof(float))
+ {
+ var floatVal = (float)currentValObj;
+ MutateFloat(ref floatVal, MinValue, MaxValue, Steps);
+ member.SetValue(plantHolder.Seed, floatVal);
+ }
+ else if (member.FieldType == typeof(int))
+ {
+ var intVal = (int)currentValObj;
+ MutateInt(ref intVal, (int)MinValue, (int)MaxValue, Steps);
+ member.SetValue(plantHolder.Seed, intVal);
+ }
+ else if (member.FieldType == typeof(bool))
+ {
+ var boolVal = (bool)currentValObj;
+ boolVal = !boolVal;
+ member.SetValue(plantHolder.Seed, boolVal);
+ }
+ }
+
+ // Mutate reference 'val' between 'min' and 'max' by pretending the value
+ // is representable by a thermometer code with 'bits' number of bits and
+ // randomly flipping some of them.
+ private void MutateFloat(ref float val, float min, float max, int bits)
+ {
+ if (min == max)
+ {
+ val = min;
+ return;
+ }
+
+ // 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);
+ // 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;
+ int valIntMutated;
+ if (Random(probIncrease))
+ {
+ valIntMutated = valInt + 1;
+ }
+ else
+ {
+ valIntMutated = valInt - 1;
+ }
+
+ // Set value based on mutated thermometer code.
+ float valMutated = Math.Clamp((float)valIntMutated / bits * (max - min) + min, min, max);
+ val = valMutated;
+ }
+
+ private void MutateInt(ref int val, int min, int max, int bits)
+ {
+ if (min == max)
+ {
+ val = min;
+ return;
+ }
+
+ // 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);
+ // 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;
+ int valMutated;
+ if (Random(probIncrease))
+ {
+ valMutated = val + 1;
+ }
+ else
+ {
+ valMutated = val - 1;
+ }
+
+ valMutated = Math.Clamp(valMutated, min, max);
+ val = valMutated;
+ }
+
+ private bool Random(float odds)
+ {
+ var random = IoCManager.Resolve();
+ return random.Prob(odds);
+ }
+
+ protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
+ {
+ throw new NotImplementedException();
+ }
+}
diff --git a/Content.Server/EntityEffects/Effects/PlantMutateChemicals.cs b/Content.Server/EntityEffects/Effects/PlantMutateChemicals.cs
new file mode 100644
index 0000000000..7ee6cd13d7
--- /dev/null
+++ b/Content.Server/EntityEffects/Effects/PlantMutateChemicals.cs
@@ -0,0 +1,55 @@
+using Content.Server.Botany;
+using Content.Server.Botany.Components;
+using Content.Shared.EntityEffects;
+using Content.Shared.Random;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+
+namespace Content.Server.EntityEffects.Effects;
+
+///
+/// changes the chemicals available in a plant's produce
+///
+public sealed partial class PlantMutateChemicals : EntityEffect
+{
+ public override void Effect(EntityEffectBaseArgs args)
+ {
+ var plantholder = args.EntityManager.GetComponent(args.TargetEntity);
+
+ if (plantholder.Seed == null)
+ return;
+
+ var random = IoCManager.Resolve();
+ var prototypeManager = IoCManager.Resolve();
+ var chemicals = plantholder.Seed.Chemicals;
+ var randomChems = prototypeManager.Index("RandomPickBotanyReagent").Fills;
+
+ // Add a random amount of a random chemical to this set of chemicals
+ if (randomChems != null)
+ {
+ var pick = random.Pick(randomChems);
+ var chemicalId = random.Pick(pick.Reagents);
+ var amount = random.Next(1, (int)pick.Quantity);
+ var seedChemQuantity = new SeedChemQuantity();
+ if (chemicals.ContainsKey(chemicalId))
+ {
+ seedChemQuantity.Min = chemicals[chemicalId].Min;
+ seedChemQuantity.Max = chemicals[chemicalId].Max + amount;
+ }
+ else
+ {
+ seedChemQuantity.Min = 1;
+ seedChemQuantity.Max = 1 + amount;
+ seedChemQuantity.Inherent = false;
+ }
+ var potencyDivisor = (int)Math.Ceiling(100.0f / seedChemQuantity.Max);
+ seedChemQuantity.PotencyDivisor = potencyDivisor;
+ chemicals[chemicalId] = seedChemQuantity;
+ }
+ }
+
+ protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
+ {
+ return "TODO";
+ }
+}
diff --git a/Content.Server/EntityEffects/Effects/PlantMutateGases.cs b/Content.Server/EntityEffects/Effects/PlantMutateGases.cs
new file mode 100644
index 0000000000..52b9da3a85
--- /dev/null
+++ b/Content.Server/EntityEffects/Effects/PlantMutateGases.cs
@@ -0,0 +1,87 @@
+using Content.Server.Botany.Components;
+using Content.Shared.Atmos;
+using Content.Shared.EntityEffects;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+using System.Linq;
+
+namespace Content.Server.EntityEffects.Effects;
+
+///
+/// changes the gases that a plant or produce create.
+///
+public sealed partial class PlantMutateExudeGasses : EntityEffect
+{
+ [DataField]
+ public float MinValue = 0.01f;
+
+ [DataField]
+ public float MaxValue = 0.5f;
+
+ public override void Effect(EntityEffectBaseArgs args)
+ {
+ var plantholder = args.EntityManager.GetComponent(args.TargetEntity);
+
+ if (plantholder.Seed == null)
+ return;
+
+ var random = IoCManager.Resolve();
+ var gasses = plantholder.Seed.ExudeGasses;
+
+ // Add a random amount of a random gas to this gas dictionary
+ float amount = random.NextFloat(MinValue, MaxValue);
+ Gas gas = random.Pick(Enum.GetValues(typeof(Gas)).Cast().ToList());
+ if (gasses.ContainsKey(gas))
+ {
+ gasses[gas] += amount;
+ }
+ else
+ {
+ gasses.Add(gas, amount);
+ }
+ }
+
+ protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
+ {
+ return "TODO";
+ }
+}
+
+///
+/// changes the gases that a plant or produce consumes.
+///
+public sealed partial class PlantMutateConsumeGasses : EntityEffect
+{
+ [DataField]
+ public float MinValue = 0.01f;
+
+ [DataField]
+ public float MaxValue = 0.5f;
+ public override void Effect(EntityEffectBaseArgs args)
+ {
+ var plantholder = args.EntityManager.GetComponent(args.TargetEntity);
+
+ if (plantholder.Seed == null)
+ return;
+
+ var random = IoCManager.Resolve();
+ var gasses = plantholder.Seed.ConsumeGasses;
+
+ // Add a random amount of a random gas to this gas dictionary
+ float amount = random.NextFloat(MinValue, MaxValue);
+ Gas gas = random.Pick(Enum.GetValues(typeof(Gas)).Cast().ToList());
+ if (gasses.ContainsKey(gas))
+ {
+ gasses[gas] += amount;
+ }
+ else
+ {
+ gasses.Add(gas, amount);
+ }
+ }
+
+ protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
+ {
+ return "TODO";
+ }
+}
diff --git a/Content.Server/EntityEffects/Effects/PlantMutateHarvest.cs b/Content.Server/EntityEffects/Effects/PlantMutateHarvest.cs
new file mode 100644
index 0000000000..e67176ee16
--- /dev/null
+++ b/Content.Server/EntityEffects/Effects/PlantMutateHarvest.cs
@@ -0,0 +1,30 @@
+using Content.Server.Botany;
+using Content.Server.Botany.Components;
+using Content.Shared.EntityEffects;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.EntityEffects.Effects;
+
+///
+/// Upgrades a plant's harvest type.
+///
+public sealed partial class PlantMutateHarvest : EntityEffect
+{
+ public override void Effect(EntityEffectBaseArgs args)
+ {
+ var plantholder = args.EntityManager.GetComponent(args.TargetEntity);
+
+ if (plantholder.Seed == null)
+ return;
+
+ if (plantholder.Seed.HarvestRepeat == HarvestType.NoRepeat)
+ plantholder.Seed.HarvestRepeat = HarvestType.Repeat;
+ else if (plantholder.Seed.HarvestRepeat == HarvestType.Repeat)
+ plantholder.Seed.HarvestRepeat = HarvestType.SelfHarvest;
+ }
+
+ protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
+ {
+ return "TODO";
+ }
+}
diff --git a/Content.Server/EntityEffects/Effects/PlantSpeciesChange.cs b/Content.Server/EntityEffects/Effects/PlantSpeciesChange.cs
new file mode 100644
index 0000000000..65bd59daa3
--- /dev/null
+++ b/Content.Server/EntityEffects/Effects/PlantSpeciesChange.cs
@@ -0,0 +1,43 @@
+using Content.Server.Botany;
+using Content.Server.Botany.Components;
+using Content.Shared.EntityEffects;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+using Serilog;
+
+namespace Content.Server.EntityEffects.Effects;
+
+///
+/// Changes a plant into one of the species its able to mutate into.
+///
+public sealed partial class PlantSpeciesChange : EntityEffect
+{
+ public override void Effect(EntityEffectBaseArgs args)
+ {
+ var prototypeManager = IoCManager.Resolve();
+ var plantholder = args.EntityManager.GetComponent(args.TargetEntity);
+
+ if (plantholder.Seed == null)
+ return;
+
+ if (plantholder.Seed.MutationPrototypes.Count == 0)
+ return;
+
+ var random = IoCManager.Resolve();
+ var targetProto = random.Pick(plantholder.Seed.MutationPrototypes);
+ prototypeManager.TryIndex(targetProto, out SeedPrototype? protoSeed);
+
+ if (protoSeed == null)
+ {
+ Log.Error($"Seed prototype could not be found: {targetProto}!");
+ return;
+ }
+
+ plantholder.Seed = plantholder.Seed.SpeciesChange(protoSeed);
+ }
+
+ protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
+ {
+ return "TODO";
+ }
+}
diff --git a/Content.Server/EntityEffects/Effects/Slipify.cs b/Content.Server/EntityEffects/Effects/Slipify.cs
new file mode 100644
index 0000000000..bc1cc062a3
--- /dev/null
+++ b/Content.Server/EntityEffects/Effects/Slipify.cs
@@ -0,0 +1,38 @@
+using Content.Shared.EntityEffects;
+using Content.Shared.Physics;
+using Content.Shared.Slippery;
+using Content.Shared.StepTrigger.Components;
+using Robust.Shared.Physics;
+using Robust.Shared.Physics.Components;
+using Robust.Shared.Physics.Systems;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.EntityEffects.Effects;
+
+///
+/// Makes a mob slippery.
+///
+public sealed partial class Slipify : EntityEffect
+{
+ public override void Effect(EntityEffectBaseArgs args)
+ {
+ var fixtureSystem = args.EntityManager.System();
+ var colWakeSystem = args.EntityManager.System();
+ var slippery = args.EntityManager.EnsureComponent(args.TargetEntity);
+ args.EntityManager.Dirty(args.TargetEntity, slippery);
+ args.EntityManager.EnsureComponent(args.TargetEntity);
+ // Need a fixture with a slip layer in order to actually do the slipping
+ var fixtures = args.EntityManager.EnsureComponent(args.TargetEntity);
+ var body = args.EntityManager.EnsureComponent(args.TargetEntity);
+ var shape = fixtures.Fixtures["fix1"].Shape;
+ fixtureSystem.TryCreateFixture(args.TargetEntity, shape, "slips", 1, false, (int)CollisionGroup.SlipLayer, manager: fixtures, body: body);
+ // Need to disable collision wake so that mobs can collide with and slip on it
+ var collisionWake = args.EntityManager.EnsureComponent(args.TargetEntity);
+ colWakeSystem.SetEnabled(args.TargetEntity, false, collisionWake);
+ }
+
+ protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
+ {
+ throw new NotImplementedException();
+ }
+}
diff --git a/Content.Shared/Random/RandomPlantMutation.cs b/Content.Shared/Random/RandomPlantMutation.cs
new file mode 100644
index 0000000000..d95cf7bf42
--- /dev/null
+++ b/Content.Shared/Random/RandomPlantMutation.cs
@@ -0,0 +1,48 @@
+using Content.Shared.EntityEffects;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Random;
+
+///
+/// Data that specifies the odds and effects of possible random plant mutations.
+///
+[Serializable, NetSerializable]
+[DataDefinition]
+public sealed partial class RandomPlantMutation
+{
+ ///
+ /// Odds of this mutation occurring with 1 point of mutation severity on a plant.
+ ///
+ [DataField]
+ public float BaseOdds = 0;
+
+ ///
+ /// The name of this mutation.
+ ///
+ [DataField]
+ public string Name = "";
+
+ ///
+ /// The actual EntityEffect to apply to the target
+ ///
+ [DataField]
+ public EntityEffect Effect = default!;
+
+ ///
+ /// This mutation will target the harvested produce
+ ///
+ [DataField]
+ public bool AppliesToProduce = true;
+
+ ///
+ /// This mutation will target the growing plant as soon as this mutation is applied.
+ ///
+ [DataField]
+ public bool AppliesToPlant = true;
+
+ ///
+ /// This mutation stays on the plant and its produce. If false while AppliesToPlant is true, the effect will run when triggered.
+ ///
+ [DataField]
+ public bool Persists = true;
+}
diff --git a/Content.Shared/Random/RandomPlantMutationListPrototype.cs b/Content.Shared/Random/RandomPlantMutationListPrototype.cs
new file mode 100644
index 0000000000..84e3b9256c
--- /dev/null
+++ b/Content.Shared/Random/RandomPlantMutationListPrototype.cs
@@ -0,0 +1,18 @@
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Random;
+
+///
+/// Random weighting dataset for solutions, able to specify reagents quantity.
+///
+[Prototype("RandomPlantMutationList")]
+public sealed partial class RandomPlantMutationListPrototype : IPrototype
+{
+ [IdDataField] public string ID { get; } = default!;
+
+ ///
+ /// List of RandomFills that can be picked from.
+ ///
+ [DataField("mutations", required: true, serverOnly: true)]
+ public List mutations = new();
+}
diff --git a/Resources/Prototypes/Hydroponics/mutations.yml b/Resources/Prototypes/Hydroponics/randomChemicals.yml
similarity index 100%
rename from Resources/Prototypes/Hydroponics/mutations.yml
rename to Resources/Prototypes/Hydroponics/randomChemicals.yml
diff --git a/Resources/Prototypes/Hydroponics/randomMutations.yml b/Resources/Prototypes/Hydroponics/randomMutations.yml
new file mode 100644
index 0000000000..50f6845ec3
--- /dev/null
+++ b/Resources/Prototypes/Hydroponics/randomMutations.yml
@@ -0,0 +1,178 @@
+- type: RandomPlantMutationList
+ id: RandomPlantMutations
+ mutations:
+ - name: Bioluminescent
+ baseOdds: 0.036
+ effect: !type:Glow
+ - name: Sentient
+ baseOdds: 0.0072
+ appliesToPlant: false # makes the botany tray sentient if true
+ effect: !type:MakeSentient # existing effect.
+ - name: Slippery
+ baseOdds: 0.036
+ effect: !type:Slipify
+ - name: ChangeSpecies
+ baseOdds: 0.036
+ appliesToProduce: false
+ effect: !type:PlantSpeciesChange
+ - name: Unviable
+ baseOdds: 0.109
+ persists: false
+ effect: !type:PlantChangeStat
+ targetValue: Viable
+ - name: ChangeWaterConsumption
+ baseOdds: 0.018
+ persists: false
+ effect: !type:PlantChangeStat
+ targetValue: WaterConsumption
+ minValue: 0.3
+ maxValue: 0.9
+ steps: 5
+ - name: ChangeNutrientConsumption
+ baseOdds: 0.018
+ persists: false
+ effect: !type:PlantChangeStat
+ targetValue: NutrientConsumption
+ minValue: 0.05
+ maxValue: 1.2
+ steps: 5
+ - name: ChangeIdealHeat
+ baseOdds: 0.018
+ persists: false
+ effect: !type:PlantChangeStat
+ targetValue: IdealHeat
+ minValue: 263
+ maxValue: 323
+ steps: 5
+ - name: ChangeHeatTolerance
+ baseOdds: 0.018
+ persists: false
+ effect: !type:PlantChangeStat
+ targetValue: HeatTolerance
+ minValue: 2
+ maxValue: 25
+ steps: 5
+ - name: ChangeToxinsTolerance
+ baseOdds: 0.018
+ persists: false
+ effect: !type:PlantChangeStat
+ targetValue: ToxinsTolerance
+ minValue: 1
+ maxValue: 10
+ steps: 5
+ - name: ChangeLowPressureTolerance
+ baseOdds: 0.018
+ persists: false
+ effect: !type:PlantChangeStat
+ targetValue: LowPressureTolerance
+ minValue: 60
+ maxValue: 100
+ steps: 5
+ - name: ChangeHighPressureTolerance
+ baseOdds: 0.018
+ persists: false
+ effect: !type:PlantChangeStat
+ targetValue: HighPressureTolerance
+ minValue: 100
+ maxValue: 140
+ steps: 5
+ - name: ChangePestTolerance
+ baseOdds: 0.018
+ persists: false
+ effect: !type:PlantChangeStat
+ targetValue: PestTolerance
+ minValue: 0
+ maxValue: 15
+ steps: 5
+ - name: ChangeWeedTolerance
+ baseOdds: 0.018
+ persists: false
+ effect: !type:PlantChangeStat
+ targetValue: WeedTolerance
+ minValue: 0
+ maxValue: 15
+ steps: 5
+ - name: ChangeEndurance
+ baseOdds: 0.036
+ persists: false
+ effect: !type:PlantChangeStat
+ targetValue: Endurance
+ minValue: 50
+ maxValue: 150
+ steps: 5
+ - name: ChangeYield
+ baseOdds: 0.036
+ persists: false
+ effect: !type:PlantChangeStat
+ targetValue: Yield
+ minValue: 3
+ maxValue: 10
+ steps: 5
+ - name: ChangeLifespan
+ baseOdds: 0.036
+ persists: false
+ effect: !type:PlantChangeStat
+ targetValue: Lifespan
+ minValue: 10
+ maxValue: 80
+ steps: 5
+ - name: ChangeMaturation
+ baseOdds: 0.036
+ persists: false
+ effect: !type:PlantChangeStat
+ targetValue: Maturation
+ minValue: 3
+ maxValue: 8
+ steps: 5
+ - name: ChangeProduction
+ baseOdds: 0.036
+ persists: false
+ effect: !type:PlantChangeStat
+ targetValue: Production
+ minValue: 1
+ maxValue: 10
+ steps: 5
+ - name: ChangePotency
+ baseOdds: 0.036
+ persists: false
+ effect: !type:PlantChangeStat
+ targetValue: Potency
+ minValue: 30
+ maxValue: 100
+ steps: 5
+ - name: ChangeSeedless
+ baseOdds: 0.036
+ persists: false
+ effect: !type:PlantChangeStat
+ targetValue: Seedless
+ - name: ChangeLigneous
+ baseOdds: 0.036
+ persists: false
+ effect: !type:PlantChangeStat
+ targetValue: Ligneous
+ - name: ChangeTurnIntoKudzu
+ baseOdds: 0.036
+ persists: false
+ effect: !type:PlantChangeStat
+ targetValue: TurnIntoKudzu
+ - name: ChangeScreaming
+ baseOdds: 0.036
+ persists: false
+ effect: !type:PlantChangeStat
+ targetValue: CanScream
+ - name: ChangeChemicals
+ baseOdds: 0.072
+ persists: false
+ effect: !type:PlantMutateChemicals
+ - name: ChangeExudeGasses
+ baseOdds: 0.0145
+ persists: false
+ effect: !type:PlantMutateExudeGasses
+ - name: ChangeConsumeGasses
+ baseOdds: 0.0036
+ persists: false
+ effect: !type:PlantMutateConsumeGasses
+ - name: ChangeHarvest
+ baseOdds: 0.036
+ persists: false
+ effect: !type:PlantMutateHarvest
\ No newline at end of file