using System.Linq; using Content.Shared.Botany.Components; using Content.Shared.Botany.Events; using Content.Shared.Botany.Traits.Components; using Content.Shared.Examine; using JetBrains.Annotations; using Robust.Shared.Timing; namespace Content.Shared.Botany.Systems; /// /// Handles plant behavior and growth processing. /// public sealed partial class PlantSystem : EntitySystem { [Dependency] private BotanySystem _botany = default!; [Dependency] private IGameTiming _gameTiming = default!; [Dependency] private PlantMutationSystem _mutation = default!; [Dependency] private PlantHolderSystem _plantHolder = default!; [Dependency] private EntityQuery _holderQuery; [Dependency] private EntityQuery _trayQuery; private readonly List> _holdersToUpdate = []; public override void Update(float frameTime) { base.Update(frameTime); _holdersToUpdate.Clear(); var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var plantHolder)) { if (plantHolder.NextUpdate > _gameTiming.CurTime) continue; plantHolder.NextUpdate = _gameTiming.CurTime; DirtyField(uid, plantHolder, nameof(plantHolder.NextUpdate)); _holdersToUpdate.Add((uid, plantHolder)); } foreach (var ent in _holdersToUpdate) { UpdatePlant(ent.AsNullable()); } } [SubscribeLocalEvent] private void OnMapInit(Entity ent, ref MapInitEvent args) { PlantingPlant(ent.AsNullable()); } [SubscribeLocalEvent] private void OnCrossPollinate(Entity ent, ref PlantCrossPollinateEvent args) { if (!_botany.TryGetPlantComponent(args.PollenData, args.PollenProtoId, out var pollenData)) return; _mutation.CrossInt(ref ent.Comp.Yield, pollenData.Yield); _mutation.CrossFloat(ref ent.Comp.Endurance, pollenData.Endurance); _mutation.CrossFloat(ref ent.Comp.Lifespan, pollenData.Lifespan); _mutation.CrossFloat(ref ent.Comp.Maturation, pollenData.Maturation); _mutation.CrossFloat(ref ent.Comp.Production, pollenData.Production); _mutation.CrossFloat(ref ent.Comp.Potency, pollenData.Potency); _mutation.CrossTrait(ent.Owner, args.PollenData); Dirty(ent); } [SubscribeLocalEvent] private void OnPlantGrow(Entity ent, ref PlantGrowEvent args) { if (!_holderQuery.TryComp(ent.Owner, out var holder)) return; // Check if plant is too old. if (holder.Age > ent.Comp.Lifespan) _plantHolder.AdjustsHealth(ent.Owner, -ent.Comp.OldAgeDamage); } [SubscribeLocalEvent] private void OnExamined(Entity ent, ref ExaminedEvent args) { if (!args.IsInDetailsRange) return; if (!_holderQuery.TryComp(ent.Owner, out var holder)) return; using (args.PushGroup(nameof(PlantComponent))) { if (_plantHolder.IsDead(ent.Owner)) args.PushMarkup(Loc.GetString("plant-component-dead-plant-matter-message")); if (_plantHolder.GetHealthThreshold(ent.Owner)) { args.PushMarkup(Loc.GetString( "plant-component-something-already-growing-low-health-message", ("healthState", Loc.GetString(holder.Age > ent.Comp.Lifespan ? "plant-component-plant-old-adjective" : "plant-component-plant-unhealthy-adjective")))); } foreach (var trait in AllComps(ent.Owner) .OrderBy(trait => trait.GetType().FullName)) { if (trait.TraitState is { } traitState) args.PushMarkup(Loc.GetString(traitState)); } } } /// /// Processes one plant's growth cycle and related effects. /// public void UpdatePlant(Entity ent, bool force = false) { if (!Resolve(ent.Owner, ref ent.Comp, false)) return; var curTime = _gameTiming.CurTime; if (!force && curTime < ent.Comp.LastCycle + ent.Comp.CycleDelay) return; ent.Comp.LastCycle = curTime; DirtyField(ent, ent.Comp, nameof(ent.Comp.LastCycle)); if (_plantHolder.IsDead(ent.Owner)) return; TryGetTray(ent.Owner, out var trayEnt); var plantGrow = new PlantGrowEvent(trayEnt.Owner); RaiseLocalEvent(ent.Owner, ref plantGrow); // Process mutations. foreach (var mutationTable in ent.Comp.MutationLevels.Keys) { var mutationBuildup = ent.Comp.MutationLevels[mutationTable]; if (mutationBuildup <= 0) continue; ent.Comp.MutationLevels[mutationTable] = 0; DirtyField(ent, ent.Comp, nameof(ent.Comp.MutationLevels)); _mutation.CheckRandomMutations(ent.Owner, mutationTable, Math.Min(mutationBuildup, ent.Comp.MaxMutationLevel)); } if (ent.Comp.Health <= 0) _plantHolder.KillPlant(ent.Owner); } /// /// Forces an update of the tray by external cause. /// [PublicAPI] public void ForceUpdate(Entity ent) { if (!Resolve(ent.Owner, ref ent.Comp, false)) return; _plantHolder.AdjustsSkipAging(ent.Owner, 1); UpdatePlant(ent.Owner, force: true); } /// /// Tries to get the tray entity that the plant is in. /// [PublicAPI] public bool TryGetTray(Entity ent, out Entity trayEnt) { trayEnt = default!; if (!Resolve(ent.Owner, ref ent.Comp, false)) return false; trayEnt.Owner = Transform(ent.Owner).ParentUid; if (!_trayQuery.TryComp(trayEnt.Owner, out var trayComp)) return false; trayEnt.Comp = trayComp; return true; } /// /// Adjusts the potency of a plant component. /// [PublicAPI] public void AdjustPotency(Entity ent, float amount) { if (!Resolve(ent.Owner, ref ent.Comp, false)) return; ent.Comp.Potency = Math.Max(0, ent.Comp.Potency + amount); DirtyField(ent, nameof(ent.Comp.Potency)); } /// /// Adjusts the lifespan of a plant component. /// [PublicAPI] public void AdjustLifespan(Entity ent, int amount) { if (!Resolve(ent.Owner, ref ent.Comp, false)) return; ent.Comp.Lifespan = Math.Max(0, ent.Comp.Lifespan + amount); DirtyField(ent, nameof(ent.Comp.Lifespan)); } /// /// Adjusts the endurance of a plant component. /// [PublicAPI] public void AdjustEndurance(Entity ent, int amount) { if (!Resolve(ent.Owner, ref ent.Comp, false)) return; ent.Comp.Endurance = MathF.Max(0f, ent.Comp.Endurance + amount); DirtyField(ent, nameof(ent.Comp.Endurance)); } /// /// Adjusts the yield of a plant component. /// [PublicAPI] public void AdjustYield(Entity ent, int amount) { if (!Resolve(ent.Owner, ref ent.Comp, false)) return; ent.Comp.Yield = Math.Max(0, ent.Comp.Yield + amount); DirtyField(ent, nameof(ent.Comp.Yield)); } /// /// Adjusts the maturation time of a plant component. /// Must be at least 1 to prevent divide-by-zero in growth stage calculations. /// [PublicAPI] public void AdjustMaturation(Entity ent, float amount) { if (!Resolve(ent.Owner, ref ent.Comp, false)) return; ent.Comp.Maturation = MathF.Max(1f, ent.Comp.Maturation + amount); DirtyField(ent, nameof(ent.Comp.Maturation)); } /// /// Adjusts the production time of a plant component. /// Must be at least 1. /// [PublicAPI] public void AdjustProduction(Entity ent, float amount) { if (!Resolve(ent.Owner, ref ent.Comp, false)) return; ent.Comp.Production = MathF.Max(1f, ent.Comp.Production + amount); DirtyField(ent, nameof(ent.Comp.Production)); } /// /// Removes the plant from the tray. /// [PublicAPI] public void RemovePlant(Entity ent) { if (!Resolve(ent.Owner, ref ent.Comp, false)) return; PredictedQueueDel(ent); } /// /// Gets the growth stage value of the plant. /// [PublicAPI] public int GetGrowthStageValue(Entity ent) { if (!Resolve(ent.Owner, ref ent.Comp, false)) return 1; if (!_holderQuery.TryComp(ent.Owner, out var plantHolder)) return 1; int growthStage; if (plantHolder.Age < ent.Comp.Maturation) growthStage = (int)(plantHolder.Age * ent.Comp.GrowthStages / ent.Comp.Maturation); else growthStage = ent.Comp.GrowthStages; return Math.Max(1, growthStage); } /// /// Planting a plant. /// [PublicAPI] public void PlantingPlant(Entity ent, float? healthOverride = null) { if (!Resolve(ent.Owner, ref ent.Comp, false)) return; if (!_holderQuery.TryComp(ent.Owner, out var plantHolder)) return; plantHolder.Health = healthOverride ?? ent.Comp.Endurance; DirtyFields(ent, plantHolder, null, nameof(plantHolder.Health)); } /// /// Gets the warnings markup of the plant. /// [PublicAPI] public string GetPlantWarningsMarkup(Entity ent) { if (!Resolve(ent.Owner, ref ent.Comp, false)) return string.Empty; var markup = new List(); if (ent.Comp.ImproperHeat) markup.Add(Loc.GetString("plant-component-heat-improper-warning")); if (ent.Comp.ImproperPressure) markup.Add(Loc.GetString("plant-component-pressure-improper-warning")); if (ent.Comp.MissingGas) markup.Add(Loc.GetString("plant-component-gas-missing-warning")); return string.Join("\n", markup); } /// /// Gets the states markup of the plant. /// [PublicAPI] public string GetPlantStateMarkup(EntityUid uid, PlantComponent? component = null) { if (component == null && !Resolve(uid, ref component, false)) return string.Empty; var markup = new List(); markup.Add(Loc.GetString("seed-component-plant-yield-text", ("seedYield", component.Yield))); markup.Add(Loc.GetString("seed-component-plant-potency-text", ("seedPotency", component.Potency))); return string.Join("\n", markup); } }