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:
@@ -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]
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
48
Content.Server/EntityEffects/Effects/Glow.cs
Normal file
48
Content.Server/EntityEffects/Effects/Glow.cs
Normal 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";
|
||||
}
|
||||
}
|
||||
142
Content.Server/EntityEffects/Effects/PlantChangeStat.cs
Normal file
142
Content.Server/EntityEffects/Effects/PlantChangeStat.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
55
Content.Server/EntityEffects/Effects/PlantMutateChemicals.cs
Normal file
55
Content.Server/EntityEffects/Effects/PlantMutateChemicals.cs
Normal 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";
|
||||
}
|
||||
}
|
||||
87
Content.Server/EntityEffects/Effects/PlantMutateGases.cs
Normal file
87
Content.Server/EntityEffects/Effects/PlantMutateGases.cs
Normal 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";
|
||||
}
|
||||
}
|
||||
30
Content.Server/EntityEffects/Effects/PlantMutateHarvest.cs
Normal file
30
Content.Server/EntityEffects/Effects/PlantMutateHarvest.cs
Normal 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";
|
||||
}
|
||||
}
|
||||
43
Content.Server/EntityEffects/Effects/PlantSpeciesChange.cs
Normal file
43
Content.Server/EntityEffects/Effects/PlantSpeciesChange.cs
Normal 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";
|
||||
}
|
||||
}
|
||||
38
Content.Server/EntityEffects/Effects/Slipify.cs
Normal file
38
Content.Server/EntityEffects/Effects/Slipify.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
48
Content.Shared/Random/RandomPlantMutation.cs
Normal file
48
Content.Shared/Random/RandomPlantMutation.cs
Normal 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;
|
||||
}
|
||||
18
Content.Shared/Random/RandomPlantMutationListPrototype.cs
Normal file
18
Content.Shared/Random/RandomPlantMutationListPrototype.cs
Normal 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();
|
||||
}
|
||||
178
Resources/Prototypes/Hydroponics/randomMutations.yml
Normal file
178
Resources/Prototypes/Hydroponics/randomMutations.yml
Normal 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
|
||||
Reference in New Issue
Block a user