Botany Rework Part 1: Mutations (#31163)

Instead of each mutation being a flag that gets checked at some unique point in BotanySystem somewhere, they're now EntityEffects that get applied when the mutation occurs and when produce is harvested. One new list was added to SeedData so that multiple other fields could be removed.

All the non-stat-change mutations that have been rolled are added to the Mutations list, and get applied to the plant when the mutation occurs or when a seed with the mutation is planted. Produce get mutations applied at harvest if they apply to the produce, and carry all of the plant's mutations over as a seed. This gets rid of the one-off checks for things like Slippery, Bioluminescent, Sentient, etc.

The base odds of a mutation applying should be equal to the odds of the original mutation check. It pretended to have 1 bit flip (on averge) per mutation power, and odds of each mutation was the odds of one of its bit being flipped (1 /275 * bits). The 'thermometer code' applied for numbers will be replaced with simple random rolls, as both average out to the middle value. The new checks are much easier to understand and don't obfuscate the actual changes of something happening behind 3 layers of math. The biggest player-facing change is that Potency will be able to get over 65 significantly more often than it did in the previous system, but it will be just as common to get low values as high ones.

Mutation definitions have been moved to a .yml file. These include the odds per tick per mutagen strength of that mutation applying that tick, the effect applied, if it applies to the plant and/or its produce. This makes mutations simpler to add and edit.

This PR is limited specifically to the mutation logic. Improving other aspects of the system will be done in other PRs per the design document. Mutations was chosen first because its got the largest amount of one-off checks scattered all over that could be consolidated. Once this is merged, mutations could be contributed to the codebase with minimal extra work for later botany refactor PRs.
This commit is contained in:
drakewill-CRL
2024-09-14 23:12:17 -04:00
committed by GitHub
parent 62eaae6504
commit 1dec19cc05
19 changed files with 812 additions and 441 deletions

View File

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

View File

@@ -13,12 +13,12 @@ public sealed partial class ProduceComponent : SharedProduceComponent
/// <summary>
/// Seed data used to create a <see cref="SeedComponent"/> when this produce has its seeds extracted.
/// </summary>
[DataField("seed")]
[DataField]
public SeedData? Seed;
/// <summary>
/// Seed data used to create a <see cref="SeedComponent"/> when this produce has its seeds extracted.
/// </summary>
[DataField("seedId", customTypeSerializer: typeof(PrototypeIdSerializer<SeedPrototype>))]
[DataField(customTypeSerializer: typeof(PrototypeIdSerializer<SeedPrototype>))]
public string? SeedId;
}

View File

@@ -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<EntityPrototype>))]
public List<string> ProductPrototypes = new();
[DataField("chemicals")] public Dictionary<string, SeedChemQuantity> Chemicals = new();
[DataField] public Dictionary<string, SeedChemQuantity> Chemicals = new();
[DataField("consumeGasses")] public Dictionary<Gas, float> ConsumeGasses = new();
[DataField] public Dictionary<Gas, float> ConsumeGasses = new();
[DataField("exudeGasses")] public Dictionary<Gas, float> ExudeGasses = new();
[DataField] public Dictionary<Gas, float> 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;
/// <summary>
/// If true, cannot be harvested for seeds. Balances hybrids and
/// mutations.
/// </summary>
[DataField("seedless")] public bool Seedless = false;
[DataField] public bool Seedless = false;
/// <summary>
/// If false, rapidly decrease health while growing. Used to kill off
/// plants with "bad" mutations.
/// </summary>
[DataField("viable")] public bool Viable = true;
/// <summary>
/// If true, fruit slips players.
/// </summary>
[DataField("slip")] public bool Slip = false;
/// <summary>
/// If true, fruits are sentient.
/// </summary>
[DataField("sentient")] public bool Sentient = false;
[DataField] public bool Viable = true;
/// <summary>
/// If true, a sharp tool is required to harvest this plant.
/// </summary>
[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";
/// <summary>
/// Screams random sound, could be strict sound SoundPathSpecifier or collection SoundCollectionSpecifier
/// base class is SoundSpecifier
/// Screams random sound from collection SoundCollectionSpecifier
/// </summary>
[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<EntityPrototype>))] public string KudzuPrototype = "WeakKudzu";
public float BioluminescentRadius = 2f;
[DataField("kudzuPrototype", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))] 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
/// <summary>
/// The mutation effects that have been applied to this plant.
/// </summary>
[DataField] public List<RandomPlantMutation> Mutations { get; set; } = new();
/// <summary>
/// The seed prototypes this seed may mutate into when prompted to.
/// </summary>
[DataField("mutationPrototypes", customTypeSerializer: typeof(PrototypeIdListSerializer<SeedPrototype>))]
[DataField(customTypeSerializer: typeof(PrototypeIdListSerializer<SeedPrototype>))]
public List<string> 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.

View File

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

View File

@@ -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<SlipperyComponent>(entity);
Dirty(entity, slippery);
EnsureComp<StepTriggerComponent>(entity);
// Need a fixture with a slip layer in order to actually do the slipping
var fixtures = EnsureComp<FixturesComponent>(entity);
var body = EnsureComp<PhysicsComponent>(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<CollisionWakeComponent>(entity);
_colWakeSystem.SetEnabled(entity, false, collisionWake);
}
}
return products;

View File

@@ -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<WeightedRandomFillSolutionPrototype>("RandomPickBotanyReagent");
_randomMutations = _prototypeManager.Index<RandomPlantMutationListPrototype>("RandomPlantMutations");
}
/// <summary>
/// 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.
/// </summary>
public void MutateSeed(ref SeedData seed, float severity)
/// <param name="seed"></param>
/// <param name="severity"></param>
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);
}
}
}
/// <summary>
/// Checks all defined mutations against a seed to see which of them are applied.
/// </summary>
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<Gas, float> 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<Gas>().ToList());
if (gasses.ContainsKey(gas))
{
gasses[gas] += amount;
}
else
{
gasses.Add(gas, amount);
}
}
private void MutateChemicals(ref Dictionary<string, SeedChemQuantity> 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>{
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<string, SeedChemQuantity> val, Dictionary<string, SeedChemQuantity> other)
{
// Go through chemicals from the pollen in swab

View File

@@ -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;
@@ -299,15 +297,6 @@ 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);
@@ -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<GhostRoleComponent>(uid);
EnsureComp<GhostTakeoverAvailableComponent>(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<PointLightComponent>(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<PointLightComponent>(uid);
}
if (!TryComp<AppearanceComponent>(uid, out var app))
return;

View File

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

View File

@@ -0,0 +1,48 @@
using Content.Shared.EntityEffects;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.EntityEffects.Effects;
/// <summary>
/// Makes a mob glow.
/// </summary>
public sealed partial class Glow : EntityEffect
{
[DataField]
public float Radius = 2f;
[DataField]
public Color Color = Color.Black;
private static readonly List<Color> 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<IRobustRandom>();
Color = random.Pick(Colors);
}
var lightSystem = args.EntityManager.System<SharedPointLightSystem>();
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";
}
}

View File

@@ -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<PlantHolderComponent>(args.TargetEntity);
if (plantHolder == null || plantHolder.Seed == null)
return;
var member = plantHolder.Seed.GetType().GetField(TargetValue);
var mutationSys = args.EntityManager.System<MutationSystem>();
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<IRobustRandom>();
return random.Prob(odds);
}
protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
{
throw new NotImplementedException();
}
}

View File

@@ -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;
/// <summary>
/// changes the chemicals available in a plant's produce
/// </summary>
public sealed partial class PlantMutateChemicals : EntityEffect
{
public override void Effect(EntityEffectBaseArgs args)
{
var plantholder = args.EntityManager.GetComponent<PlantHolderComponent>(args.TargetEntity);
if (plantholder.Seed == null)
return;
var random = IoCManager.Resolve<IRobustRandom>();
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
var chemicals = plantholder.Seed.Chemicals;
var randomChems = prototypeManager.Index<WeightedRandomFillSolutionPrototype>("RandomPickBotanyReagent").Fills;
// Add a random amount of a random chemical to this set of chemicals
if (randomChems != null)
{
var pick = random.Pick<RandomFillSolution>(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";
}
}

View File

@@ -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;
/// <summary>
/// changes the gases that a plant or produce create.
/// </summary>
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<PlantHolderComponent>(args.TargetEntity);
if (plantholder.Seed == null)
return;
var random = IoCManager.Resolve<IRobustRandom>();
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<Gas>().ToList());
if (gasses.ContainsKey(gas))
{
gasses[gas] += amount;
}
else
{
gasses.Add(gas, amount);
}
}
protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
{
return "TODO";
}
}
/// <summary>
/// changes the gases that a plant or produce consumes.
/// </summary>
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<PlantHolderComponent>(args.TargetEntity);
if (plantholder.Seed == null)
return;
var random = IoCManager.Resolve<IRobustRandom>();
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<Gas>().ToList());
if (gasses.ContainsKey(gas))
{
gasses[gas] += amount;
}
else
{
gasses.Add(gas, amount);
}
}
protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
{
return "TODO";
}
}

View File

@@ -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;
/// <summary>
/// Upgrades a plant's harvest type.
/// </summary>
public sealed partial class PlantMutateHarvest : EntityEffect
{
public override void Effect(EntityEffectBaseArgs args)
{
var plantholder = args.EntityManager.GetComponent<PlantHolderComponent>(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";
}
}

View File

@@ -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;
/// <summary>
/// Changes a plant into one of the species its able to mutate into.
/// </summary>
public sealed partial class PlantSpeciesChange : EntityEffect
{
public override void Effect(EntityEffectBaseArgs args)
{
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
var plantholder = args.EntityManager.GetComponent<PlantHolderComponent>(args.TargetEntity);
if (plantholder.Seed == null)
return;
if (plantholder.Seed.MutationPrototypes.Count == 0)
return;
var random = IoCManager.Resolve<IRobustRandom>();
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";
}
}

View File

@@ -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;
/// <summary>
/// Makes a mob slippery.
/// </summary>
public sealed partial class Slipify : EntityEffect
{
public override void Effect(EntityEffectBaseArgs args)
{
var fixtureSystem = args.EntityManager.System<FixtureSystem>();
var colWakeSystem = args.EntityManager.System<CollisionWakeSystem>();
var slippery = args.EntityManager.EnsureComponent<SlipperyComponent>(args.TargetEntity);
args.EntityManager.Dirty(args.TargetEntity, slippery);
args.EntityManager.EnsureComponent<StepTriggerComponent>(args.TargetEntity);
// Need a fixture with a slip layer in order to actually do the slipping
var fixtures = args.EntityManager.EnsureComponent<FixturesComponent>(args.TargetEntity);
var body = args.EntityManager.EnsureComponent<PhysicsComponent>(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<CollisionWakeComponent>(args.TargetEntity);
colWakeSystem.SetEnabled(args.TargetEntity, false, collisionWake);
}
protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
{
throw new NotImplementedException();
}
}

View File

@@ -0,0 +1,48 @@
using Content.Shared.EntityEffects;
using Robust.Shared.Serialization;
namespace Content.Shared.Random;
/// <summary>
/// Data that specifies the odds and effects of possible random plant mutations.
/// </summary>
[Serializable, NetSerializable]
[DataDefinition]
public sealed partial class RandomPlantMutation
{
/// <summary>
/// Odds of this mutation occurring with 1 point of mutation severity on a plant.
/// </summary>
[DataField]
public float BaseOdds = 0;
/// <summary>
/// The name of this mutation.
/// </summary>
[DataField]
public string Name = "";
/// <summary>
/// The actual EntityEffect to apply to the target
/// </summary>
[DataField]
public EntityEffect Effect = default!;
/// <summary>
/// This mutation will target the harvested produce
/// </summary>
[DataField]
public bool AppliesToProduce = true;
/// <summary>
/// This mutation will target the growing plant as soon as this mutation is applied.
/// </summary>
[DataField]
public bool AppliesToPlant = true;
/// <summary>
/// This mutation stays on the plant and its produce. If false while AppliesToPlant is true, the effect will run when triggered.
/// </summary>
[DataField]
public bool Persists = true;
}

View File

@@ -0,0 +1,18 @@
using Robust.Shared.Prototypes;
namespace Content.Shared.Random;
/// <summary>
/// Random weighting dataset for solutions, able to specify reagents quantity.
/// </summary>
[Prototype("RandomPlantMutationList")]
public sealed partial class RandomPlantMutationListPrototype : IPrototype
{
[IdDataField] public string ID { get; } = default!;
/// <summary>
/// List of RandomFills that can be picked from.
/// </summary>
[DataField("mutations", required: true, serverOnly: true)]
public List<RandomPlantMutation> mutations = new();
}

View File

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