using System.Linq; using Content.Shared.Atmos; using Content.Shared.Botany.Components; using Content.Shared.Botany.Traits.Components; using Content.Shared.Chemistry.Reagent; using Content.Shared.EntityEffects; using JetBrains.Annotations; using Robust.Shared.Network; using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Serialization.Manager; namespace Content.Shared.Botany.Systems; /// /// Handles plant mutations, including random mutation effects, crossbreeding, and /// inheritance of plant properties and traits from pollen. /// public sealed partial class PlantMutationSystem : EntitySystem { [Dependency] private INetManager _net = default!; [Dependency] private IRobustRandom _random = default!; [Dependency] private BotanySystem _botany = default!; [Dependency] private ISerializationManager _serialization = default!; [Dependency] private PlantSystem _plant = default!; [Dependency] private PlantTraySystem _plantTray = default!; [Dependency] private SharedEntityEffectsSystem _entityEffects = default!; [Dependency] private EntityQuery _chemicalsQuery; [Dependency] private EntityQuery _plantQuery; /// /// For each mutation table, go through each mutation. /// For each random mutation, see if it occurs on this plant this check. /// [PublicAPI] public void CheckRandomMutations(Entity ent, ProtoId mutationTableId, float severity) { if (!Resolve(ent, ref ent.Comp, false)) return; var mutationTable = ProtoMan.Index(mutationTableId); foreach (var mutation in mutationTable.Mutations) { var triggers = 0; while (mutation.BaseOdds * severity > triggers) // If the odds are greater than one, potentially trigger the mutation multiple times { var odds = mutation.BaseOdds * severity - triggers; triggers++; if (Random(Math.Min(odds, 1.0f))) { if (mutation.AppliesToPlant) _entityEffects.TryApplyEffect(ent, mutation.Effect); // Stat adjustments do not persist by being an attached effect, they just change the stat. if (mutation.Persists && ent.Comp.Mutations.All(m => m.Name != mutation.Name)) ent.Comp.Mutations.Add(mutation); } } } } /// /// Replaces the current plant species with a new one from prototype, /// preserving lifecycle state. /// [PublicAPI] public void SpeciesChange(Entity oldPlant, EntProtoId newPlantProto) { if (!Resolve(oldPlant, ref oldPlant.Comp, false)) return; if (oldPlant.Comp.MutationPrototypes.Count == 0) return; if (!_net.IsServer) return; // Clone state via snapshot and apply to new plant. var snapshot = _botany.ClonePlantSnapshotData(oldPlant.Owner, cloneLifecycle: true); if (snapshot == null) return; var newPlantUid = SpawnAtPosition(newPlantProto, Transform(oldPlant.Owner).Coordinates); _botany.ApplyPlantSnapshotData(snapshot, newPlantUid, cloneLifecycle: true); _botany.DeletePlantSnapshot(snapshot); ChemicalsSpeciesChange(newPlantUid, newPlantProto); PlantSpeciesChange(newPlantUid, newPlantProto); if (_plant.TryGetTray(oldPlant.Owner, out var trayEnt)) _plantTray.PlantingPlantInTray(trayEnt.AsNullable(), newPlantUid); else _plant.PlantingPlant(newPlantUid); _plant.ForceUpdate(newPlantUid); QueueDel(oldPlant); } private void ChemicalsSpeciesChange(EntityUid plantUid, EntProtoId plantProto) { if (!_botany.TryGetPlantComponent(null, plantProto, out var newPlantChemicals) || !_chemicalsQuery.TryComp(plantUid, out var oldPlantChemicals)) return; var oldPlant = oldPlantChemicals.Chemicals; var newPlant = newPlantChemicals.Chemicals; // Adding the new chemicals from the new species. foreach (var otherChem in newPlant) { oldPlant.TryAdd(otherChem.Key, otherChem.Value); } // Removing the inherent chemicals from the old species. Leaving mutated/crossbred ones intact. foreach (var originalChem in oldPlant) { if (!newPlant.ContainsKey(originalChem.Key) && originalChem.Value.Inherent) oldPlant.Remove(originalChem.Key); } Dirty(plantUid, oldPlantChemicals); } /// /// Copies information from the PlantComponent on a prototype over to a given plant. /// /// /// must stay true for the given plant. /// TODO: better support excluding fields depending on clone context. /// private void PlantSpeciesChange(EntityUid plantUid, EntProtoId plantProto) { if (!_botany.TryGetPlantComponent(null, plantProto, out var newSpecies) || !_plantQuery.TryComp(plantUid, out var plant)) return; plant.GrowthStages = newSpecies.GrowthStages; DirtyField(plantUid, plant, nameof(PlantComponent.GrowthStages)); } /// /// Combines mutations from the pollen and target plants. /// [PublicAPI] public void CrossMutations(EntityUid pollenPlant, EntProtoId? pollenProtoId, EntityUid targetPlant) { if (!_botany.TryGetPlantComponent(pollenPlant, pollenProtoId, out var pollenCore) || !_plantQuery.TryComp(targetPlant, out var targetCore)) return; // 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. targetCore.Mutations = targetCore.Mutations.Where(_ => Random(0.5f)).Union(pollenCore.Mutations.Where(_ => Random(0.5f))).DistinctBy(m => m.Name).ToList(); // Hybrids have a high chance of being seedless. Balances very // effective hybrid crossings. if (pollenProtoId != null && pollenProtoId != MetaData(targetPlant).EntityPrototype?.ID && Random(0.7f)) { EnsureComp(targetPlant); } } /// /// Combines chemical properties from the pollen and target plants. /// [PublicAPI] public void CrossChemicals(EntityUid uid, ref Dictionary, PlantChemQuantity> val, Dictionary, PlantChemQuantity> other) { // Go through chemicals from the pollen in swab foreach (var otherChem in other) { // if both have same chemical, randomly pick potency ratio from the two. if (val.TryGetValue(otherChem.Key, out var value)) { val[otherChem.Key] = Random(0.5f) ? otherChem.Value : value; } // if target plant doesn't have this chemical, has 50% chance to add it. else { if (Random(0.5f)) { var fixedChem = otherChem.Value; fixedChem.Inherent = false; val.Add(otherChem.Key, fixedChem); } } } // if the target plant has chemical that the pollen in swab does not, 50% chance to remove it. foreach (var thisChem in val) { if (!other.ContainsKey(thisChem.Key)) { if (Random(0.5f)) { if (val.Count > 1) { val.Remove(thisChem.Key); } } } } } /// /// Combines gas properties from the pollen and target plants. /// [PublicAPI] public void CrossGasses(EntityUid uid, ref Dictionary val, Dictionary other) { // Go through gasses from the pollen in swab foreach (var otherGas in other) { // if both have same gas, randomly pick ammount from the two. if (val.TryGetValue(otherGas.Key, out var value)) { val[otherGas.Key] = Random(0.5f) ? otherGas.Value : value; } // if target plant doesn't have this gas, has 50% chance to add it. else { if (Random(0.5f)) { val.Add(otherGas.Key, otherGas.Value); } } } // if the target plant has gas that the pollen in swab does not, 50% chance to remove it. foreach (var thisGas in val) { if (!other.ContainsKey(thisGas.Key)) { if (Random(0.5f)) { val.Remove(thisGas.Key); } } } } /// /// Selects a floating value from the plant or pollen. /// [PublicAPI] public void CrossFloat(ref float val, float other) { val = Random(0.5f) ? val : other; } /// /// Selects an integer value from the plant or pollen. /// [PublicAPI] public void CrossInt(ref int val, int other) { val = Random(0.5f) ? val : other; } /// /// Selects a Boolean value from the plant or pollen. /// [PublicAPI] public void CrossBool(ref bool val, bool other) { val = Random(0.5f) ? val : other; } /// /// Crosses plant trait components from pollen to the target plant. /// [PublicAPI] public void CrossTrait(EntityUid val, EntityUid pollenData) { foreach (var component in AllComps(pollenData)) { if (component is not PlantTraitsComponent) continue; if (HasComp(val, component.GetType())) continue; if (Random(0.5f)) AddComp(val, _serialization.CreateCopy(component, notNullableOverride: true)); } } private bool Random(float p) { // TODO: A reliable method for PredictedRandom in the engine is needed. if (!_net.IsServer) return false; return _random.Prob(p); } }