using Content.Shared.Administration.Logs; using Content.Shared.Botany.Components; using Content.Shared.Botany.Events; using Content.Shared.Database; using Content.Shared.Interaction; using Content.Shared.Popups; using JetBrains.Annotations; namespace Content.Shared.Botany.Systems; /// /// Manages harvest readiness and execution for plants, including repeat/self-harvest /// logic and produce spawning, responding to growth and interaction events. /// public sealed partial class PlantHarvestSystem : EntitySystem { [Dependency] private BotanySystem _botany = default!; [Dependency] private ISharedAdminLogManager _adminLogger = default!; [Dependency] private PlantSystem _plant = default!; [Dependency] private SharedPopupSystem _popup = default!; [Dependency] private PlantHolderSystem _plantHolder = default!; [Dependency] private PlantTraySystem _plantTray = default!; [Dependency] private EntityQuery _holderQuery; [Dependency] private EntityQuery _harvestQuery; [Dependency] private EntityQuery _plantQuery; [Dependency] private EntityQuery _dataQuery; [SubscribeLocalEvent] private void OnInteractHand(Entity ent, ref InteractHandEvent args) { if (args.Handled) return; // TODO: Remove this once trays have a proper UI. if (!_plantTray.TryGetAlivePlant(ent.AsNullable(), out var plantUid)) return; if (TryHandleHarvest(plantUid.Value, args.User)) args.Handled = true; } [SubscribeLocalEvent] private void OnPlantGrow(Entity ent, ref PlantGrowEvent args) { if (!_harvestQuery.TryComp(ent.Owner, out var harvest) || !_plantQuery.TryComp(ent.Owner, out var plant)) return; // If the plant is not mature, set the last harvest to the current age. if (ent.Comp.Age < plant.Maturation) ent.Comp.LastHarvest = ent.Comp.Age; TryAutoHarvest((ent, harvest), ent.Owner); // Update whether the plant is ready for harvest. var timeLastHarvest = ent.Comp.Age - ent.Comp.LastHarvest; if (timeLastHarvest > plant.Production && !ent.Comp.ReadyForHarvest) { ent.Comp.ReadyForHarvest = true; ent.Comp.LastHarvest = ent.Comp.Age; } Dirty(ent); } [SubscribeLocalEvent] private void OnInteractHand(Entity ent, ref InteractHandEvent args) { if (args.Handled) return; if (_plantHolder.IsDead(ent.Owner)) { args.Handled = true; _plant.RemovePlant(ent.Owner); return; } if (TryHandleHarvest(ent, args.User)) args.Handled = true; } private void TryAutoHarvest(Entity ent, EntityUid user) { if (ent.Comp.HarvestRepeat != HarvestType.SelfHarvest) return; if (_dataQuery.TryComp(ent.Owner, out var plantData) && plantData.HarvestLogImpact != null) _adminLogger.Add(LogType.Botany, plantData.HarvestLogImpact.Value, $"Auto-harvested {Loc.GetString(plantData.Name):seed} at Pos:{Transform(ent).Coordinates}."); DoHarvest(ent.Owner, user); } /// /// Attempts to harvest the plant, raising and harvesting if not cancelled. /// /// True if the harvest was handled, even if cancelled. [PublicAPI] public bool TryHandleHarvest(EntityUid plant, EntityUid user, EntityUid? used = null) { if (!_holderQuery.TryComp(plant, out var holder) || !holder.ReadyForHarvest) return false; var ev = new PlantHarvestAttemptEvent(user, plant, used); RaiseLocalEvent(plant, ref ev); if (ev.Cancelled) return true; if (_dataQuery.TryComp(plant, out var plantData) && plantData.HarvestLogImpact != null) _adminLogger.Add(LogType.Botany, plantData.HarvestLogImpact.Value, $"{ToPrettyString(user):player} harvested {Loc.GetString(plantData.Name):seed} at Pos:{Transform(user).Coordinates}."); DoHarvest(plant, user); return true; } /// /// Harvests the plant and produces the produce. /// [PublicAPI] public void DoHarvest(Entity ent, EntityUid user) { if (!Resolve(ent.Owner, ref ent.Comp, false)) return; if (!_plantQuery.TryComp(ent.Owner, out var plant) || !_dataQuery.TryComp(ent.Owner, out var plantData) || !_harvestQuery.TryComp(ent.Owner, out var harvest)) return; if (!ent.Comp.ReadyForHarvest || plantData.ProductPrototypes.Count == 0 || plant.Yield == 0) return; var name = Loc.GetString(plantData.Name); _popup.PopupCursor(Loc.GetString("botany-harvest-success-message", ("name", name)), user, PopupType.Medium); var totalYield = 0; if (plant.Yield >= 0) { totalYield = ent.Comp.YieldMod < 0 ? plant.Yield : plant.Yield * ent.Comp.YieldMod; totalYield = Math.Max(1, totalYield); } var position = Transform(user).Coordinates; _botany.SpawnProduce(ent.Owner, position, totalYield); ent.Comp.ReadyForHarvest = false; ent.Comp.LastHarvest = ent.Comp.Age; Dirty(ent); if (harvest.HarvestRepeat == HarvestType.NoRepeat) _plant.RemovePlant(ent.Owner); var ev = new PlantHarvestedEvent(user, ent.Owner); RaiseLocalEvent(ent.Owner, ref ev); } /// /// Resets harvest progress to the plant's current age. /// [PublicAPI] public void ResetHarvestProgress(Entity ent) { if (!Resolve(ent.Owner, ref ent.Comp, false)) return; ent.Comp.LastHarvest = ent.Comp.Age; DirtyField(ent, nameof(ent.Comp.LastHarvest)); } /// /// Affects the growth of a plant by modifying its age or production timing. /// [PublicAPI] public void AffectGrowth(Entity ent, int amount) { if (amount == 0) return; if (!Resolve(ent.Owner, ref ent.Comp, false)) return; if (!_plantQuery.TryComp(ent.Owner, out var plant)) return; if (amount > 0) { if (ent.Comp.Age < plant.Maturation) _plantHolder.AdjustsAge(ent.Owner, amount); else if (!ent.Comp.ReadyForHarvest && plant.Yield <= 0f) ent.Comp.LastHarvest -= amount; } else { if (ent.Comp.Age < plant.Maturation) _plantHolder.AdjustsSkipAging(ent.Owner, 1); else if (!ent.Comp.ReadyForHarvest && plant.Yield <= 0f) ent.Comp.LastHarvest += amount; } DirtyField(ent, nameof(ent.Comp.LastHarvest)); } /// /// Changes the harvest repeat of a plant. /// [PublicAPI] public void ChangeHarvestRepeat(Entity ent) { if (!Resolve(ent, ref ent.Comp, false)) return; ent.Comp.HarvestRepeat = ent.Comp.HarvestRepeat switch { HarvestType.NoRepeat => HarvestType.Repeat, HarvestType.Repeat => HarvestType.SelfHarvest, _ => ent.Comp.HarvestRepeat, }; DirtyField(ent, nameof(ent.Comp.HarvestRepeat)); } }