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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user