mirror of
https://github.com/space-syndicate/space-station-14.git
synced 2026-10-09 10:21:18 +03:00
409 lines
13 KiB
C#
409 lines
13 KiB
C#
using System.Diagnostics.CodeAnalysis;
|
|
using Content.Shared.Botany.Components;
|
|
using Content.Shared.Chemistry.EntitySystems;
|
|
using Content.Shared.EntityEffects;
|
|
using Content.Shared.Examine;
|
|
using Content.Shared.FixedPoint;
|
|
using Content.Shared.Interaction;
|
|
using Content.Shared.Random.Helpers;
|
|
using JetBrains.Annotations;
|
|
using Robust.Shared.Audio.Systems;
|
|
using Robust.Shared.Containers;
|
|
using Robust.Shared.Timing;
|
|
|
|
namespace Content.Shared.Botany.Systems;
|
|
|
|
/// <summary>
|
|
/// Handles plant tray state, including plant management, resource consumption,
|
|
/// reagent processing, and periodic tray updates.
|
|
/// </summary>
|
|
public sealed partial class PlantTraySystem : EntitySystem
|
|
{
|
|
[Dependency] private IGameTiming _timing = default!;
|
|
[Dependency] private PlantHolderSystem _plantHolder = default!;
|
|
[Dependency] private PlantSystem _plant = default!;
|
|
[Dependency] private SharedAudioSystem _audio = default!;
|
|
[Dependency] private SharedEntityEffectsSystem _entityEffects = default!;
|
|
[Dependency] private SharedInteractionSystem _interaction = default!;
|
|
[Dependency] private SharedSolutionContainerSystem _solutionContainer = default!;
|
|
[Dependency] private SharedTransformSystem _transform = default!;
|
|
|
|
[Dependency] private EntityQuery<PlantTrayComponent> _trayQuery;
|
|
[Dependency] private EntityQuery<PlantDataComponent> _dataQuery;
|
|
|
|
[SubscribeLocalEvent]
|
|
private void OnExamine(Entity<PlantTrayComponent> ent, ref ExaminedEvent args)
|
|
{
|
|
if (!args.IsInDetailsRange)
|
|
return;
|
|
|
|
using (args.PushGroup(nameof(PlantTrayComponent)))
|
|
{
|
|
if (!TryGetPlant(ent.AsNullable(), out var plantUid))
|
|
{
|
|
args.PushMarkup(Loc.GetString("tray-component-nothing-planted-message"));
|
|
if (_dataQuery.TryComp(plantUid, out var plantData))
|
|
{
|
|
var name = Loc.GetString(plantData.Name);
|
|
args.PushMarkup(Loc.GetString("plant-component-something-already-growing-message", ("seedName", name)));
|
|
}
|
|
}
|
|
|
|
args.PushMarkup(Loc.GetString("tray-component-water-level-message",
|
|
("waterLevel", (int)ent.Comp.WaterLevel)));
|
|
args.PushMarkup(Loc.GetString("tray-component-nutrient-level-message",
|
|
("nutritionLevel", (int)ent.Comp.NutritionLevel)));
|
|
|
|
args.PushMarkup(GetTrayWarningsMarkup(ent.AsNullable()));
|
|
if (plantUid != null && ent.Comp.DrawWarnings)
|
|
args.PushMarkup(_plant.GetPlantWarningsMarkup(plantUid.Value));
|
|
}
|
|
}
|
|
|
|
[SubscribeLocalEvent]
|
|
private void OnSolutionTransferred(Entity<PlantTrayComponent> ent, ref SolutionTransferredEvent args)
|
|
{
|
|
_audio.PlayPredicted(ent.Comp.WateringSound, ent, args.User);
|
|
}
|
|
|
|
// TODO: replace with relationship system when it will be merged.
|
|
// this is really cursed - handler is getting hits on client when plant getting into tray
|
|
// but not when it gets out, yet it works just fine on server.
|
|
[SubscribeLocalEvent]
|
|
private void OnPlantTerminating(Entity<PlantComponent> ent, ref EntityTerminatingEvent args)
|
|
{
|
|
var trayUid = Transform(ent).ParentUid;
|
|
|
|
if (!_trayQuery.TryComp(trayUid, out var tray)
|
|
|| tray.PlantEntity != ent.Owner)
|
|
{
|
|
return;
|
|
}
|
|
|
|
tray.PlantEntity = null;
|
|
DirtyField(trayUid, tray, nameof(tray.PlantEntity));
|
|
}
|
|
|
|
// Workaround for https://github.com/space-wizards/space-station-14/pull/35314
|
|
[SubscribeLocalEvent]
|
|
private void OnEntRemoved(Entity<PlantTrayComponent> ent, ref EntRemovedFromContainerMessage args)
|
|
{
|
|
// Make sure the removed entity was our contained solution and clear our cached reference
|
|
if (args.Entity == ent.Comp.SoilSolution?.Owner)
|
|
ent.Comp.SoilSolution = null;
|
|
}
|
|
|
|
[SubscribeLocalEvent]
|
|
private void OnRelayInteractUsing(Entity<PlantTrayComponent> ent,
|
|
ref PlantHolderRelayedEvent<InteractUsingEvent> args)
|
|
{
|
|
if (args.Args.Handled)
|
|
return;
|
|
|
|
args.Args.Handled = _interaction.InteractUsing(args.Args.User, args.Args.Used, ent, args.Args.ClickLocation);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates trays whose periodic processing is due.
|
|
/// </summary>
|
|
public override void Update(float frameTime)
|
|
{
|
|
base.Update(frameTime);
|
|
|
|
var query = EntityQueryEnumerator<PlantTrayComponent>();
|
|
while (query.MoveNext(out var uid, out var tray))
|
|
{
|
|
if (tray.NextUpdate > _timing.CurTime)
|
|
continue;
|
|
|
|
tray.NextUpdate = _timing.CurTime + tray.UpdateDelay;
|
|
DirtyField(uid, tray, nameof(tray.NextUpdate));
|
|
UpdateReagents(uid);
|
|
GrowthWeeds(uid);
|
|
|
|
var ev = new TrayUpdateEvent();
|
|
RaiseLocalEvent(uid, ref ev);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the reagents of the tray.
|
|
/// </summary>
|
|
[PublicAPI]
|
|
public void UpdateReagents(Entity<PlantTrayComponent?> ent)
|
|
{
|
|
var (trayUid, trayComp) = ent;
|
|
|
|
if (!Resolve(trayUid, ref trayComp, false))
|
|
return;
|
|
|
|
if (!_solutionContainer.ResolveSolution(trayUid, trayComp.SoilSolutionName, ref trayComp.SoilSolution, out var solution))
|
|
return;
|
|
|
|
if (solution.Volume <= 0)
|
|
return;
|
|
|
|
TryGetPlant(ent, out var plantUid);
|
|
var contents = trayComp.SoilSolution.Value.Comp.Solution.Contents.ToArray();
|
|
foreach (var entry in contents)
|
|
{
|
|
if (entry.Quantity.Float() >= 1f)
|
|
{
|
|
var reagentProto = ProtoMan.Index(entry.Reagent.Prototype);
|
|
_entityEffects.ApplyEffects(trayUid, [.. reagentProto.PlantMetabolisms], entry.Quantity.Float());
|
|
|
|
if (plantUid != null)
|
|
{
|
|
_entityEffects.ApplyEffects(plantUid.Value, [.. reagentProto.PlantMetabolisms], entry.Quantity.Float());
|
|
}
|
|
}
|
|
}
|
|
|
|
_solutionContainer.RemoveEachReagent(trayComp.SoilSolution.Value, FixedPoint2.New(1));
|
|
}
|
|
|
|
private void GrowthWeeds(Entity<PlantTrayComponent?> ent)
|
|
{
|
|
if (!Resolve(ent.Owner, ref ent.Comp, false))
|
|
return;
|
|
|
|
if (ent.Comp is not { WaterLevel: > 10, NutritionLevel: > 5 })
|
|
return;
|
|
|
|
if (SharedRandomExtensions.PredictedProb(_timing, ent.Comp.WeedGrowthChance, GetNetEntity(ent)))
|
|
AdjustWeed(ent, ent.Comp.WeedGrowthAmount);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Planting a plant in a tray.
|
|
/// </summary>
|
|
[PublicAPI]
|
|
public void PlantingPlantInTray(Entity<PlantTrayComponent?> trayEnt, EntityUid plantUid, float? healthOverride = null)
|
|
{
|
|
var (trayUid, trayComp) = trayEnt;
|
|
|
|
if (!Resolve(trayUid, ref trayComp, false))
|
|
return;
|
|
|
|
_plant.PlantingPlant(plantUid, healthOverride);
|
|
_transform.SetCoordinates(plantUid, Transform(trayUid).Coordinates);
|
|
_transform.SetParent(plantUid, trayUid);
|
|
trayComp.PlantEntity = plantUid;
|
|
DirtyField(trayEnt, nameof(trayComp.PlantEntity));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adjusts the nutrient level of the tray.
|
|
/// </summary>
|
|
[PublicAPI]
|
|
public void AdjustNutrient(Entity<PlantTrayComponent?> ent, float amount)
|
|
{
|
|
if (!Resolve(ent.Owner, ref ent.Comp, false))
|
|
return;
|
|
|
|
ent.Comp.NutritionLevel += amount;
|
|
ent.Comp.NutritionLevel = MathHelper.Clamp(ent.Comp.NutritionLevel, 0f, ent.Comp.MaxNutritionLevel);
|
|
DirtyField(ent, nameof(ent.Comp.NutritionLevel));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adjusts the water level of the tray.
|
|
/// </summary>
|
|
[PublicAPI]
|
|
public void AdjustWater(Entity<PlantTrayComponent?> ent, float amount)
|
|
{
|
|
if (!Resolve(ent.Owner, ref ent.Comp, false))
|
|
return;
|
|
|
|
ent.Comp.WaterLevel += amount;
|
|
ent.Comp.WaterLevel = MathHelper.Clamp(ent.Comp.WaterLevel, 0f, ent.Comp.MaxWaterLevel);
|
|
DirtyField(ent, nameof(ent.Comp.WaterLevel));
|
|
|
|
// Water dilutes toxins.
|
|
if (amount > 0)
|
|
AdjustToxin(ent, -amount * 4f);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adjusts the pest level of the tray.
|
|
/// </summary>
|
|
[PublicAPI]
|
|
public void AdjustPest(Entity<PlantTrayComponent?> ent, float amount)
|
|
{
|
|
if (!Resolve(ent.Owner, ref ent.Comp, false))
|
|
return;
|
|
|
|
ent.Comp.PestLevel = MathHelper.Clamp(ent.Comp.PestLevel + amount, 0f, ent.Comp.MaxPestLevel);
|
|
DirtyField(ent, nameof(ent.Comp.PestLevel));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adjusts the toxin level of the tray.
|
|
/// </summary>
|
|
[PublicAPI]
|
|
public void AdjustToxin(Entity<PlantTrayComponent?> ent, float amount)
|
|
{
|
|
if (!Resolve(ent.Owner, ref ent.Comp, false))
|
|
return;
|
|
|
|
ent.Comp.ToxinLevel = MathHelper.Clamp(ent.Comp.ToxinLevel + amount, 0f, ent.Comp.MaxToxinLevel);
|
|
DirtyField(ent, nameof(ent.Comp.ToxinLevel));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adjusts the weed level of the tray.
|
|
/// </summary>
|
|
[PublicAPI]
|
|
public void AdjustWeed(Entity<PlantTrayComponent?> ent, float amount)
|
|
{
|
|
if (!Resolve(ent.Owner, ref ent.Comp, false))
|
|
return;
|
|
|
|
ent.Comp.WeedLevel += amount * ent.Comp.WeedCoefficient;
|
|
ent.Comp.WeedLevel = MathHelper.Clamp(ent.Comp.WeedLevel, 0f, ent.Comp.MaxWeedLevel);
|
|
DirtyField(ent, nameof(ent.Comp.WeedLevel));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks whether the tray's toxin level has reached half its maximum.
|
|
/// </summary>
|
|
[PublicAPI]
|
|
public bool GetToxinThreshold(Entity<PlantTrayComponent?> ent)
|
|
{
|
|
if (!Resolve(ent.Owner, ref ent.Comp, false))
|
|
return false;
|
|
|
|
return ent.Comp.ToxinLevel >= ent.Comp.MaxToxinLevel * 0.5f;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks whether the tray contains a plant entity.
|
|
/// </summary>
|
|
public bool HasPlant(Entity<PlantTrayComponent?> ent)
|
|
{
|
|
return TryGetPlant(ent, out _);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tries to get the plant entity in the tray.
|
|
/// </summary>
|
|
[PublicAPI]
|
|
public bool TryGetPlant(Entity<PlantTrayComponent?> ent, [NotNullWhen(true)] out EntityUid? plant)
|
|
{
|
|
plant = null;
|
|
if (!Resolve(ent.Owner, ref ent.Comp, false))
|
|
return false;
|
|
|
|
plant = ent.Comp.PlantEntity;
|
|
if (plant == null || Deleted(plant))
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks whether the tray contains a living plant entity.
|
|
/// </summary>
|
|
public bool HasAlivePlant(Entity<PlantTrayComponent?> ent)
|
|
{
|
|
return TryGetAlivePlant(ent, out _);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tries to get the living plant entity in the tray.
|
|
/// </summary>
|
|
[PublicAPI]
|
|
public bool TryGetAlivePlant(Entity<PlantTrayComponent?> ent, [NotNullWhen(true)] out EntityUid? plant)
|
|
{
|
|
plant = null;
|
|
if (!Resolve(ent.Owner, ref ent.Comp))
|
|
return false;
|
|
|
|
if (!TryGetPlant(ent.Owner, out plant))
|
|
return false;
|
|
|
|
return !_plantHolder.IsDead(plant.Value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the warnings markup of the tray.
|
|
/// </summary>
|
|
[PublicAPI]
|
|
public string GetTrayWarningsMarkup(Entity<PlantTrayComponent?> ent)
|
|
{
|
|
if (!Resolve(ent.Owner, ref ent.Comp, false))
|
|
return string.Empty;
|
|
|
|
var markup = new List<string>();
|
|
if (GetWeedThreshold(ent))
|
|
markup.Add(Loc.GetString("tray-component-weed-high-level-warning"));
|
|
|
|
if (GetWaterThreshold(ent))
|
|
markup.Add(Loc.GetString("tray-component-water-low-warning"));
|
|
|
|
if (GetNutrientThreshold(ent))
|
|
markup.Add(Loc.GetString("tray-component-nutrient-low-warning"));
|
|
|
|
if (GetToxinThreshold(ent))
|
|
markup.Add(Loc.GetString("tray-component-toxin-high-level-warning"));
|
|
|
|
if (GetPestThreshold(ent))
|
|
markup.Add(Loc.GetString("tray-component-pest-high-level-warning"));
|
|
|
|
return string.Join("\n", markup);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks whether the tray's weed level has reached half its maximum.
|
|
/// </summary>
|
|
[PublicAPI]
|
|
public bool GetWeedThreshold(Entity<PlantTrayComponent?> ent)
|
|
{
|
|
if (!Resolve(ent.Owner, ref ent.Comp, false))
|
|
return false;
|
|
|
|
return ent.Comp.WeedLevel >= ent.Comp.MaxWeedLevel * 0.5f;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks whether the tray's pest level has reached half its maximum.
|
|
/// </summary>
|
|
[PublicAPI]
|
|
public bool GetPestThreshold(Entity<PlantTrayComponent?> ent)
|
|
{
|
|
if (!Resolve(ent.Owner, ref ent.Comp, false))
|
|
return false;
|
|
|
|
return ent.Comp.PestLevel >= ent.Comp.MaxPestLevel * 0.5f;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks whether the tray's water level is critically low.
|
|
/// </summary>
|
|
[PublicAPI]
|
|
public bool GetWaterThreshold(Entity<PlantTrayComponent?> ent)
|
|
{
|
|
if (!Resolve(ent.Owner, ref ent.Comp, false))
|
|
return false;
|
|
|
|
return ent.Comp.WaterLevel <= ent.Comp.MaxWaterLevel * 0.1f;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks whether the tray's nutrient level is critically low.
|
|
/// </summary>
|
|
[PublicAPI]
|
|
public bool GetNutrientThreshold(Entity<PlantTrayComponent?> ent)
|
|
{
|
|
if (!Resolve(ent.Owner, ref ent.Comp, false))
|
|
return false;
|
|
|
|
return ent.Comp.NutritionLevel <= ent.Comp.MaxNutritionLevel * 0.1f;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Event raised when a tray is updated.
|
|
/// </summary>
|
|
[ByRefEvent]
|
|
public readonly record struct TrayUpdateEvent;
|