* A big hecking chemistry-related refactor. Changed SolutionContainerCaps. It now describes "stock" behavior for interacting with solutions that is pre-implemented by SolutionContainerComponent. As such things like syringes do not check it anymore (on themselves) to see "can we remove reagent from ourselves". That's assumed by it... being a syringe. SolutionContainerCaps now has different flags more accurately describing possible reagent interaction behaviors. ISolutionInteractionsComponent is the interface that describes the common behaviors like "what happens when injected with a syringe". This is implemented by SolutionContainerComponent but could be implemented by other classes. One notable example that drove me to making this interface was the /vg/station circuit imprinter which splits reagent poured in into its two reservoir beakers. Having this interface allows us to do this "proxying" behavior hack-free. (the hacks in /vg/ code were somewhat dirty...). PourableComponent has been replaced SolutionTransferComponent. It now describes both give-and-take behavior for the common reagent containers. This is in line with /vg/'s /obj/item/weapon/reagent_containers architecture. "Taking" in this context is ONLY from reagent tanks like fuel tanks. Oh, should I mention that fuel tanks and such have a proper component now? They do. Because of this behavioral change, reagent tanks DO NOT have Pourable anymore. Removing from reagent tanks is now in the hands of the item used on them. Welders and fire extinguishers now have code for removing from them. This sounds bad at first but remember that all have quite unique behavior related to this: Welders cause explosions if lit and can ONLY be fueled at fuel tanks. Extinguishers can be filled at any tank, etc... The code for this is also simpler due to ISolutionInteractionsComponent now so... IAfterInteract now works like IInteractUsing with the Priority levels and "return true to block further handlers" behavior. This was necessary to make extinguishers prioritize taking from tanks over spraying. Explicitly coded interactions like welders refueling also means they refuse instantly to full now, which they didn't before. And it plays the sound. Etc... Probably more stuff I'm forgetting. * Review improvements.
893 lines
30 KiB
C#
893 lines
30 KiB
C#
#nullable enable
|
|
using System;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using Content.Server.Atmos;
|
|
using Content.Server.Botany;
|
|
using Content.Server.GameObjects.Components.Chemistry;
|
|
using Content.Server.GameObjects.Components.Fluids;
|
|
using Content.Server.GameObjects.Components.GUI;
|
|
using Content.Server.GameObjects.EntitySystems;
|
|
using Content.Server.Utility;
|
|
using Content.Shared.Audio;
|
|
using Content.Shared.Chemistry;
|
|
using Content.Shared.GameObjects.Components.Botany;
|
|
using Content.Shared.GameObjects.Components.Chemistry;
|
|
using Content.Shared.GameObjects.EntitySystems;
|
|
using Content.Shared.GameObjects.EntitySystems.ActionBlocker;
|
|
using Content.Shared.Interfaces;
|
|
using Content.Shared.Interfaces.GameObjects.Components;
|
|
using Content.Shared.Utility;
|
|
using Robust.Server.GameObjects;
|
|
using Robust.Server.GameObjects.EntitySystems;
|
|
using Robust.Shared.GameObjects;
|
|
using Robust.Shared.GameObjects.ComponentDependencies;
|
|
using Robust.Shared.GameObjects.Systems;
|
|
using Robust.Shared.Interfaces.GameObjects;
|
|
using Robust.Shared.Interfaces.Random;
|
|
using Robust.Shared.Interfaces.Timing;
|
|
using Robust.Shared.IoC;
|
|
using Robust.Shared.Localization;
|
|
using Robust.Shared.Maths;
|
|
using Robust.Shared.Prototypes;
|
|
using Robust.Shared.Random;
|
|
using Robust.Shared.Serialization;
|
|
using Robust.Shared.Utility;
|
|
using Robust.Shared.ViewVariables;
|
|
|
|
namespace Content.Server.GameObjects.Components.Botany
|
|
{
|
|
[RegisterComponent]
|
|
public class PlantHolderComponent : Component, IInteractUsing, IInteractHand, IActivate, IReagentReaction, IExamine
|
|
{
|
|
public const float HydroponicsSpeedMultiplier = 1f;
|
|
public const float HydroponicsConsumptionMultiplier = 4f;
|
|
|
|
[Dependency] private readonly IRobustRandom _random = default!;
|
|
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
|
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
|
|
|
public override string Name => "PlantHolder";
|
|
|
|
[ViewVariables] private int _lastProduce;
|
|
[ViewVariables(VVAccess.ReadWrite)] private int _missingGas;
|
|
private readonly TimeSpan _cycleDelay = TimeSpan.FromSeconds(15f);
|
|
[ViewVariables] private TimeSpan _lastCycle = TimeSpan.Zero;
|
|
[ViewVariables(VVAccess.ReadWrite)] private bool _updateSpriteAfterUpdate;
|
|
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public bool DrawWarnings { get; private set; } = false;
|
|
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public float WaterLevel { get; private set; } = 100f;
|
|
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public float NutritionLevel { get; private set; } = 100f;
|
|
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public float PestLevel { get; set; }
|
|
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public float WeedLevel { get; set; }
|
|
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public float Toxins { get; set; }
|
|
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public int Age { get; set; }
|
|
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public int SkipAging { get; set; }
|
|
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public bool Dead { get; set; }
|
|
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public bool Harvest { get; set; }
|
|
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public bool Sampled { get; set; }
|
|
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public int YieldMod { get; set; } = 1;
|
|
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public float MutationMod { get; set; } = 1f;
|
|
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public float MutationLevel { get; set; }
|
|
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public float Health { get; set; }
|
|
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public float WeedCoefficient { get; set; } = 1f;
|
|
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public Seed? Seed { get; set; }
|
|
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public bool ImproperHeat { get; set; }
|
|
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public bool ImproperPressure { get; set; }
|
|
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public bool ImproperLight { get; set; }
|
|
|
|
[ViewVariables(VVAccess.ReadWrite)]
|
|
public bool ForceUpdate { get; set; }
|
|
|
|
[ComponentDependency] private readonly SolutionContainerComponent? _solutionContainer = default!;
|
|
[ComponentDependency] private readonly AppearanceComponent? _appearanceComponent = default!;
|
|
|
|
public override void Initialize()
|
|
{
|
|
base.Initialize();
|
|
|
|
Owner.EnsureComponentWarn<SolutionContainerComponent>();
|
|
}
|
|
|
|
public override void ExposeData(ObjectSerializer serializer)
|
|
{
|
|
base.ExposeData(serializer);
|
|
serializer.DataField(this, x => x.DrawWarnings, "drawWarnings", false);
|
|
}
|
|
|
|
public void WeedInvasion()
|
|
{
|
|
// TODO
|
|
}
|
|
|
|
public void Update()
|
|
{
|
|
UpdateReagents();
|
|
|
|
var curTime = _gameTiming.CurTime;
|
|
|
|
if (ForceUpdate)
|
|
ForceUpdate = false;
|
|
else if (curTime < (_lastCycle + _cycleDelay))
|
|
{
|
|
if(_updateSpriteAfterUpdate)
|
|
UpdateSprite();
|
|
return;
|
|
}
|
|
|
|
_lastCycle = curTime;
|
|
|
|
|
|
// Weeds like water and nutrients! They may appear even if there's not a seed planted.
|
|
if (WaterLevel > 10 && NutritionLevel > 2 && _random.Prob(Seed == null ? 0.05f : 0.01f))
|
|
{
|
|
WeedLevel += 1 * HydroponicsSpeedMultiplier * WeedCoefficient;
|
|
|
|
if (DrawWarnings)
|
|
_updateSpriteAfterUpdate = true;
|
|
}
|
|
|
|
// There's a chance for a weed explosion to happen if weeds take over.
|
|
// Plants that are themselves weeds (WeedTolerance > 8) are unaffected.
|
|
if (WeedLevel >= 10 && _random.Prob(0.1f))
|
|
{
|
|
if (Seed == null || WeedLevel >= Seed.WeedTolerance + 2)
|
|
WeedInvasion();
|
|
}
|
|
|
|
// If we have no seed planted, or the plant is dead, stop processing here.
|
|
if (Seed == null || Dead)
|
|
{
|
|
if (_updateSpriteAfterUpdate)
|
|
UpdateSprite();
|
|
|
|
return;
|
|
}
|
|
|
|
// There's a small chance the pest population increases.
|
|
// Can only happen when there's a live seed planted.
|
|
if (_random.Prob(0.01f))
|
|
{
|
|
PestLevel += 0.5f * HydroponicsSpeedMultiplier;
|
|
if (DrawWarnings)
|
|
_updateSpriteAfterUpdate = true;
|
|
}
|
|
|
|
// Advance plant age here.
|
|
if (SkipAging > 0)
|
|
SkipAging--;
|
|
else
|
|
{
|
|
if(_random.Prob(0.8f))
|
|
Age += (int)(1 * HydroponicsSpeedMultiplier);
|
|
|
|
_updateSpriteAfterUpdate = true;
|
|
}
|
|
|
|
// Nutrient consumption.
|
|
if (Seed.NutrientConsumption > 0 && NutritionLevel > 0 && _random.Prob(0.75f))
|
|
{
|
|
NutritionLevel -= MathF.Max(0f, Seed.NutrientConsumption * HydroponicsSpeedMultiplier);
|
|
if (DrawWarnings)
|
|
_updateSpriteAfterUpdate = true;
|
|
}
|
|
|
|
// Water consumption.
|
|
if (Seed.WaterConsumption > 0 && WaterLevel > 0 && _random.Prob(0.75f))
|
|
{
|
|
WaterLevel -= MathF.Max(0f, Seed.NutrientConsumption * HydroponicsConsumptionMultiplier * HydroponicsSpeedMultiplier);
|
|
if (DrawWarnings)
|
|
_updateSpriteAfterUpdate = true;
|
|
}
|
|
|
|
var healthMod = _random.Next(1, 3) * HydroponicsSpeedMultiplier;
|
|
|
|
// Make sure the plant is not starving.
|
|
if (_random.Prob(0.35f))
|
|
{
|
|
if (NutritionLevel > 2)
|
|
{
|
|
Health += healthMod;
|
|
}
|
|
else
|
|
{
|
|
AffectGrowth(-1);
|
|
Health -= healthMod;
|
|
}
|
|
|
|
if (DrawWarnings)
|
|
_updateSpriteAfterUpdate = true;
|
|
}
|
|
|
|
// Make sure the plant is not thirsty.
|
|
if (_random.Prob(0.35f))
|
|
{
|
|
if (WaterLevel > 10)
|
|
{
|
|
Health += healthMod;
|
|
}
|
|
else
|
|
{
|
|
AffectGrowth(-1);
|
|
Health -= healthMod;
|
|
}
|
|
|
|
if (DrawWarnings)
|
|
_updateSpriteAfterUpdate = true;
|
|
}
|
|
|
|
var tileAtmos = Owner.Transform.Coordinates.GetTileAtmosphere();
|
|
var environment = tileAtmos?.Air ?? GasMixture.SpaceGas;
|
|
|
|
if (Seed.ConsumeGasses.Count > 0)
|
|
{
|
|
_missingGas = 0;
|
|
|
|
foreach (var (gas, amount) in Seed.ConsumeGasses)
|
|
{
|
|
if (environment.GetMoles(gas) < amount)
|
|
{
|
|
_missingGas++;
|
|
continue;
|
|
}
|
|
|
|
environment.AdjustMoles(gas, -amount);
|
|
}
|
|
|
|
if (_missingGas > 0)
|
|
{
|
|
Health -= _missingGas * HydroponicsSpeedMultiplier;
|
|
if (DrawWarnings)
|
|
_updateSpriteAfterUpdate = true;
|
|
}
|
|
}
|
|
|
|
// Seed pressure resistance.
|
|
var pressure = environment.Pressure;
|
|
if (pressure < Seed.LowPressureTolerance || pressure > Seed.HighPressureTolerance)
|
|
{
|
|
Health -= healthMod;
|
|
ImproperPressure = true;
|
|
if (DrawWarnings)
|
|
_updateSpriteAfterUpdate = true;
|
|
}
|
|
else
|
|
{
|
|
ImproperPressure = false;
|
|
}
|
|
|
|
// Seed ideal temperature.
|
|
if (MathF.Abs(environment.Temperature - Seed.IdealHeat) > Seed.HeatTolerance)
|
|
{
|
|
Health -= healthMod;
|
|
ImproperHeat = true;
|
|
if (DrawWarnings)
|
|
_updateSpriteAfterUpdate = true;
|
|
}
|
|
else
|
|
{
|
|
ImproperHeat = false;
|
|
}
|
|
|
|
// Gas production.
|
|
var exudeCount = Seed.ExudeGasses.Count;
|
|
if (exudeCount > 0)
|
|
{
|
|
foreach (var (gas, amount) in Seed.ExudeGasses)
|
|
{
|
|
environment.AdjustMoles(gas, MathF.Max(1f, MathF.Round((amount * MathF.Round(Seed.Potency)) / exudeCount)));
|
|
}
|
|
}
|
|
|
|
// Toxin levels beyond the plant's tolerance cause damage.
|
|
// They are, however, slowly reduced over time.
|
|
if (Toxins > 0)
|
|
{
|
|
var toxinUptake = MathF.Max(1, MathF.Round(Toxins / 10f));
|
|
if (Toxins > Seed.ToxinsTolerance)
|
|
{
|
|
Health -= toxinUptake;
|
|
}
|
|
|
|
Toxins -= toxinUptake;
|
|
if (DrawWarnings)
|
|
_updateSpriteAfterUpdate = true;
|
|
}
|
|
|
|
// Weed levels.
|
|
if (PestLevel > 0)
|
|
{
|
|
// TODO: Carnivorous plants?
|
|
if (PestLevel > Seed.PestTolerance)
|
|
{
|
|
Health -= HydroponicsSpeedMultiplier;
|
|
}
|
|
|
|
if (DrawWarnings)
|
|
_updateSpriteAfterUpdate = true;
|
|
}
|
|
|
|
// Weed levels.
|
|
if (WeedLevel > 0)
|
|
{
|
|
// TODO: Parasitic plants.
|
|
if (WeedLevel >= Seed.WeedTolerance)
|
|
{
|
|
Health -= HydroponicsSpeedMultiplier;
|
|
}
|
|
|
|
if (DrawWarnings)
|
|
_updateSpriteAfterUpdate = true;
|
|
}
|
|
|
|
if (Age > Seed.Lifespan)
|
|
{
|
|
Health -= _random.Next(3, 5) * HydroponicsSpeedMultiplier;
|
|
if (DrawWarnings)
|
|
_updateSpriteAfterUpdate = true;
|
|
}
|
|
else if(Age < 0) // Revert back to seed packet!
|
|
{
|
|
Seed.SpawnSeedPacket(Owner.Transform.Coordinates);
|
|
RemovePlant();
|
|
ForceUpdate = true;
|
|
Update();
|
|
}
|
|
|
|
CheckHealth();
|
|
|
|
if (Harvest && Seed.HarvestRepeat == HarvestType.SelfHarvest)
|
|
AutoHarvest();
|
|
|
|
// If enough time has passed since the plant was harvested, we're ready to harvest again!
|
|
if (!Dead && Seed.ProductPrototypes.Count > 0)
|
|
{
|
|
if (Age > Seed.Production)
|
|
{
|
|
if ((Age - _lastProduce) > Seed.Production && !Harvest)
|
|
{
|
|
Harvest = true;
|
|
_lastProduce = Age;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (Harvest)
|
|
{
|
|
Harvest = false;
|
|
_lastProduce = Age;
|
|
}
|
|
}
|
|
}
|
|
|
|
CheckLevelSanity();
|
|
|
|
if(_updateSpriteAfterUpdate)
|
|
UpdateSprite();
|
|
}
|
|
|
|
private void CheckLevelSanity()
|
|
{
|
|
if (Seed != null)
|
|
Health = MathHelper.Clamp(Health, 0, Seed.Endurance);
|
|
else
|
|
{
|
|
Health = 0f;
|
|
Dead = false;
|
|
}
|
|
|
|
MutationLevel = MathHelper.Clamp(MutationLevel, 0f, 100f);
|
|
NutritionLevel = MathHelper.Clamp(NutritionLevel, 0f, 100f);
|
|
WaterLevel = MathHelper.Clamp(WaterLevel, 0f, 100f);
|
|
PestLevel = MathHelper.Clamp(PestLevel, 0f, 10f);
|
|
WeedLevel = MathHelper.Clamp(WeedLevel, 0f, 10f);
|
|
Toxins = MathHelper.Clamp(Toxins, 0f, 100f);
|
|
YieldMod = MathHelper.Clamp(YieldMod, 0, 2);
|
|
MutationMod = MathHelper.Clamp(MutationMod, 0f, 3f);
|
|
}
|
|
|
|
public bool DoHarvest(IEntity user)
|
|
{
|
|
if (Seed == null || user.Deleted || !ActionBlockerSystem.CanInteract(user))
|
|
return false;
|
|
|
|
if (Harvest && !Dead)
|
|
{
|
|
if (user.TryGetComponent(out HandsComponent? hands))
|
|
{
|
|
if (!Seed.CheckHarvest(user, hands.GetActiveHand?.Owner))
|
|
return false;
|
|
|
|
} else if (!Seed.CheckHarvest(user))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Seed.Harvest(user, YieldMod);
|
|
AfterHarvest();
|
|
return true;
|
|
}
|
|
|
|
if (!Dead) return false;
|
|
|
|
RemovePlant();
|
|
AfterHarvest();
|
|
return true;
|
|
}
|
|
|
|
public void AutoHarvest()
|
|
{
|
|
if (Seed == null || !Harvest)
|
|
return;
|
|
|
|
Seed.AutoHarvest(Owner.Transform.Coordinates);
|
|
AfterHarvest();
|
|
}
|
|
|
|
private void AfterHarvest()
|
|
{
|
|
Harvest = false;
|
|
_lastProduce = Age;
|
|
|
|
if(Seed?.HarvestRepeat == HarvestType.NoRepeat)
|
|
RemovePlant();
|
|
|
|
CheckLevelSanity();
|
|
UpdateSprite();
|
|
}
|
|
|
|
public void CheckHealth()
|
|
{
|
|
if (Health <= 0)
|
|
{
|
|
Die();
|
|
}
|
|
}
|
|
|
|
public void Die()
|
|
{
|
|
Dead = true;
|
|
Harvest = false;
|
|
MutationLevel = 0;
|
|
YieldMod = 1;
|
|
MutationMod = 1;
|
|
ImproperLight = false;
|
|
ImproperHeat = false;
|
|
ImproperPressure = false;
|
|
WeedLevel += 1 * HydroponicsSpeedMultiplier;
|
|
PestLevel = 0;
|
|
UpdateSprite();
|
|
}
|
|
|
|
public void RemovePlant()
|
|
{
|
|
YieldMod = 1;
|
|
MutationMod = 1;
|
|
PestLevel = 0;
|
|
Seed = null;
|
|
Dead = false;
|
|
Age = 0;
|
|
Sampled = false;
|
|
Harvest = false;
|
|
ImproperLight = false;
|
|
ImproperPressure = false;
|
|
ImproperHeat = false;
|
|
|
|
UpdateSprite();
|
|
}
|
|
|
|
public void AffectGrowth(int amount)
|
|
{
|
|
if (Seed == null)
|
|
return;
|
|
|
|
if (amount > 0)
|
|
{
|
|
if (Age < Seed.Maturation)
|
|
Age += amount;
|
|
else if (!Harvest && Seed.Yield <= 0f)
|
|
_lastProduce -= amount;
|
|
}
|
|
else
|
|
{
|
|
if (Age < Seed.Maturation)
|
|
SkipAging++;
|
|
else if (!Harvest && Seed.Yield <= 0f)
|
|
_lastProduce += amount;
|
|
}
|
|
}
|
|
|
|
public void AdjustNutrient(float amount)
|
|
{
|
|
NutritionLevel += amount;
|
|
}
|
|
|
|
public void AdjustWater(float amount)
|
|
{
|
|
WaterLevel += amount;
|
|
|
|
// Water dilutes toxins.
|
|
if (amount > 0)
|
|
{
|
|
Toxins -= amount * 4f;
|
|
}
|
|
}
|
|
|
|
public void UpdateReagents()
|
|
{
|
|
if (_solutionContainer == null)
|
|
return;
|
|
|
|
if (_solutionContainer.Solution.TotalVolume <= 0 || MutationLevel >= 25)
|
|
{
|
|
if (MutationLevel >= 0)
|
|
{
|
|
Mutate(Math.Min(MutationLevel, 25));
|
|
MutationLevel = 0;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var one = ReagentUnit.New(1);
|
|
|
|
foreach (var (reagent, amount) in _solutionContainer.ReagentList.ToArray())
|
|
{
|
|
var reagentProto = _prototypeManager.Index<ReagentPrototype>(reagent);
|
|
reagentProto.ReactionPlant(Owner);
|
|
_solutionContainer.Solution.RemoveReagent(reagent, amount < one ? amount : one);
|
|
}
|
|
}
|
|
|
|
CheckLevelSanity();
|
|
}
|
|
|
|
private void Mutate(float severity)
|
|
{
|
|
// TODO: Coming soon in "Botany 2: Plant boogaloo".
|
|
}
|
|
|
|
public void UpdateSprite()
|
|
{
|
|
_updateSpriteAfterUpdate = false;
|
|
|
|
if (_appearanceComponent == null)
|
|
return;
|
|
|
|
if (Seed != null)
|
|
{
|
|
if(DrawWarnings)
|
|
_appearanceComponent.SetData(PlantHolderVisuals.HealthLight, Health <= (Seed.Endurance / 2f));
|
|
|
|
if (Dead)
|
|
{
|
|
_appearanceComponent.SetData(PlantHolderVisuals.Plant, new SpriteSpecifier.Rsi(Seed.PlantRsi, "dead"));
|
|
}
|
|
else if (Harvest)
|
|
{
|
|
_appearanceComponent.SetData(PlantHolderVisuals.Plant, new SpriteSpecifier.Rsi(Seed.PlantRsi, "harvest"));
|
|
}
|
|
else if (Age < Seed.Maturation)
|
|
{
|
|
var growthStage = Math.Max(1, (int)((Age * Seed.GrowthStages) / Seed.Maturation));
|
|
_appearanceComponent.SetData(PlantHolderVisuals.Plant, new SpriteSpecifier.Rsi(Seed.PlantRsi,$"stage-{growthStage}"));
|
|
_lastProduce = Age;
|
|
}
|
|
else
|
|
{
|
|
_appearanceComponent.SetData(PlantHolderVisuals.Plant, new SpriteSpecifier.Rsi(Seed.PlantRsi,$"stage-{Seed.GrowthStages}"));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
_appearanceComponent.SetData(PlantHolderVisuals.Plant, SpriteSpecifier.Invalid);
|
|
_appearanceComponent.SetData(PlantHolderVisuals.HealthLight, false);
|
|
}
|
|
|
|
if (!DrawWarnings) return;
|
|
_appearanceComponent.SetData(PlantHolderVisuals.WaterLight, WaterLevel <= 10);
|
|
_appearanceComponent.SetData(PlantHolderVisuals.NutritionLight, NutritionLevel <= 2);
|
|
_appearanceComponent.SetData(PlantHolderVisuals.AlertLight, WeedLevel >= 5 || PestLevel >= 5 || Toxins >= 40 || ImproperHeat || ImproperLight || ImproperPressure || _missingGas > 0);
|
|
_appearanceComponent.SetData(PlantHolderVisuals.HarvestLight, Harvest);
|
|
}
|
|
|
|
public void CheckForDivergence(bool modified)
|
|
{
|
|
// Make sure we're not modifying a "global" seed.
|
|
// If this seed is not in the global seed list, then no products of this line have been harvested yet.
|
|
// It is then safe to assume it's restricted to this tray.
|
|
if (Seed == null) return;
|
|
var plantSystem = EntitySystem.Get<PlantSystem>();
|
|
if (plantSystem.Seeds.ContainsKey(Seed.Uid))
|
|
Seed = Seed.Diverge(modified);
|
|
}
|
|
|
|
private void ForceUpdateByExternalCause()
|
|
{
|
|
SkipAging++; // We're forcing an update cycle, so one age hasn't passed.
|
|
ForceUpdate = true;
|
|
Update();
|
|
}
|
|
|
|
public async Task<bool> InteractUsing(InteractUsingEventArgs eventArgs)
|
|
{
|
|
var user = eventArgs.User;
|
|
var usingItem = eventArgs.Using;
|
|
|
|
if (usingItem == null || usingItem.Deleted || !ActionBlockerSystem.CanInteract(user))
|
|
return false;
|
|
|
|
if (usingItem.TryGetComponent(out SeedComponent? seeds))
|
|
{
|
|
if (Seed == null)
|
|
{
|
|
if (seeds.Seed == null)
|
|
{
|
|
user.PopupMessageCursor(Loc.GetString("The packet seems to be empty. You throw it away."));
|
|
usingItem.Delete();
|
|
return false;
|
|
}
|
|
|
|
user.PopupMessageCursor(Loc.GetString("You plant the {0} {1}.", seeds.Seed.SeedName, seeds.Seed.SeedNoun));
|
|
|
|
Seed = seeds.Seed;
|
|
Dead = false;
|
|
Age = 1;
|
|
Health = Seed.Endurance;
|
|
_lastCycle = _gameTiming.CurTime;
|
|
|
|
usingItem.Delete();
|
|
|
|
CheckLevelSanity();
|
|
UpdateSprite();
|
|
|
|
return true;
|
|
}
|
|
|
|
user.PopupMessageCursor(Loc.GetString("The {0} already has seeds in it!", Owner.Name));
|
|
return false;
|
|
}
|
|
|
|
if (usingItem.HasComponent<HoeComponent>())
|
|
{
|
|
if (WeedLevel > 0)
|
|
{
|
|
user.PopupMessageCursor(Loc.GetString("You remove the weeds from the {0}.", Owner.Name));
|
|
user.PopupMessageOtherClients(Loc.GetString("{0} starts uprooting the weeds.", user.Name));
|
|
WeedLevel = 0;
|
|
UpdateSprite();
|
|
}
|
|
else
|
|
{
|
|
user.PopupMessageCursor(Loc.GetString("This plot is devoid of weeds! It doesn't need uprooting."));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
if (usingItem.HasComponent<ShovelComponent>())
|
|
{
|
|
if (Seed != null)
|
|
{
|
|
user.PopupMessageCursor(Loc.GetString("You remove the plant from the {0}.", Owner.Name));
|
|
user.PopupMessageOtherClients(Loc.GetString("{0} removes the plant.", user.Name));
|
|
RemovePlant();
|
|
}
|
|
else
|
|
{
|
|
user.PopupMessageCursor(Loc.GetString("There is no plant to remove."));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
if (usingItem.TryGetComponent(out ISolutionInteractionsComponent? solution) && solution.CanDrain)
|
|
{
|
|
var amount = ReagentUnit.New(5);
|
|
var sprayed = false;
|
|
|
|
if (usingItem.TryGetComponent(out SprayComponent? spray))
|
|
{
|
|
sprayed = true;
|
|
amount = ReagentUnit.New(1);
|
|
EntitySystem.Get<AudioSystem>().PlayFromEntity(spray.SpraySound, usingItem, AudioHelpers.WithVariation(0.125f));
|
|
}
|
|
|
|
var split = solution.Drain(amount);
|
|
if (split.TotalVolume == 0)
|
|
{
|
|
user.PopupMessageCursor(Loc.GetString("{0:TheName} is empty!", usingItem));
|
|
return true;
|
|
}
|
|
|
|
user.PopupMessageCursor(Loc.GetString(
|
|
sprayed ? "You spray {0:TheName}" : "You transfer {1}u to {0:TheName}",
|
|
Owner, split.TotalVolume));
|
|
|
|
_solutionContainer?.TryAddSolution(split);
|
|
|
|
ForceUpdateByExternalCause();
|
|
|
|
return true;
|
|
}
|
|
|
|
if (usingItem.HasComponent<PlantSampleTakerComponent>())
|
|
{
|
|
if (Seed == null)
|
|
{
|
|
user.PopupMessageCursor(Loc.GetString("There is nothing to take a sample of!"));
|
|
return false;
|
|
}
|
|
|
|
if (Sampled)
|
|
{
|
|
user.PopupMessageCursor(Loc.GetString("This plant has already been sampled."));
|
|
return false;
|
|
}
|
|
|
|
if (Dead)
|
|
{
|
|
user.PopupMessageCursor(Loc.GetString("This plant is dead."));
|
|
return false;
|
|
}
|
|
|
|
var seed = Seed.SpawnSeedPacket(user.Transform.Coordinates);
|
|
seed.RandomOffset(0.25f);
|
|
user.PopupMessageCursor(Loc.GetString($"You take a sample from the {Seed.DisplayName}."));
|
|
Health -= (_random.Next(3, 5) * 10);
|
|
|
|
if (_random.Prob(0.3f))
|
|
Sampled = true;
|
|
|
|
// Just in case.
|
|
CheckLevelSanity();
|
|
ForceUpdateByExternalCause();
|
|
|
|
return true;
|
|
}
|
|
|
|
if (usingItem.HasComponent<BotanySharpComponent>())
|
|
{
|
|
return DoHarvest(user);
|
|
}
|
|
|
|
if (usingItem.HasComponent<ProduceComponent>())
|
|
{
|
|
user.PopupMessageCursor(Loc.GetString("You compost {1:theName} into {0:theName}.", Owner, usingItem));
|
|
user.PopupMessageOtherClients(Loc.GetString("{0:TheName} composts {1:theName} into {2:theName}.", user, usingItem, Owner));
|
|
|
|
if (usingItem.TryGetComponent(out SolutionContainerComponent? solution2))
|
|
{
|
|
// This deliberately discards overfill.
|
|
_solutionContainer?.TryAddSolution(solution2.SplitSolution(solution2.Solution.TotalVolume));
|
|
|
|
ForceUpdateByExternalCause();
|
|
}
|
|
|
|
usingItem.Delete();
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public ReagentUnit ReagentReactTouch(ReagentPrototype reagent, ReagentUnit volume)
|
|
{
|
|
if(_solutionContainer == null)
|
|
return ReagentUnit.Zero;
|
|
|
|
_solutionContainer.TryAddReagent(reagent.ID, volume, out var accepted);
|
|
return accepted;
|
|
}
|
|
|
|
public ReagentUnit ReagentReactInjection(ReagentPrototype reagent, ReagentUnit volume)
|
|
{
|
|
if(_solutionContainer == null)
|
|
return ReagentUnit.Zero;
|
|
|
|
_solutionContainer.TryAddReagent(reagent.ID, volume, out var accepted);
|
|
return accepted;
|
|
}
|
|
|
|
public bool InteractHand(InteractHandEventArgs eventArgs)
|
|
{
|
|
// DoHarvest does the sanity checks.
|
|
return DoHarvest(eventArgs.User);
|
|
}
|
|
|
|
public void Activate(ActivateEventArgs eventArgs)
|
|
{
|
|
// DoHarvest does the sanity checks.
|
|
DoHarvest(eventArgs.User);
|
|
}
|
|
|
|
public void Examine(FormattedMessage message, bool inDetailsRange)
|
|
{
|
|
if (!inDetailsRange)
|
|
return;
|
|
|
|
if (Seed == null)
|
|
{
|
|
message.AddMarkup(Loc.GetString("It has nothing planted in it.\n"));
|
|
}
|
|
else if (!Dead)
|
|
{
|
|
message.AddMarkup(Loc.GetString($"[color=green]{Seed.DisplayName}[/color] {(Seed.DisplayName.EndsWith('s') ? "are" : "is")} growing here.\n"));
|
|
|
|
if(Health <= Seed.Endurance / 2)
|
|
message.AddMarkup(Loc.GetString($"The plant looks [color=red]{(Age > Seed.Lifespan ? "old and wilting" : "unhealthy")}[/color].\n"));
|
|
}
|
|
else
|
|
{
|
|
message.AddMarkup(Loc.GetString("It is full of [color=red]dead plant matter[/color].\n"));
|
|
}
|
|
|
|
if(WeedLevel >= 5)
|
|
message.AddMarkup(Loc.GetString("It is filled with [color=green]weeds[/color]!\n"));
|
|
|
|
if(PestLevel >= 5)
|
|
message.AddMarkup(Loc.GetString("It is filled with [color=gray]tiny worms[/color]!\n"));
|
|
|
|
message.AddMarkup(Loc.GetString($"Water: [color=cyan]{(int)WaterLevel}[/color]\n"));
|
|
message.AddMarkup(Loc.GetString($"Nutrient: [color=orange]{(int)NutritionLevel}[/color]\n"));
|
|
|
|
if (DrawWarnings)
|
|
{
|
|
if(Toxins > 40f)
|
|
message.AddMarkup(Loc.GetString("The [color=red]toxicity level alert[/color] is flashing red.\n"));
|
|
|
|
if(ImproperLight)
|
|
message.AddMarkup(Loc.GetString("The [color=yellow]improper light level alert[/color] is blinking.\n"));
|
|
|
|
if(ImproperHeat)
|
|
message.AddMarkup(Loc.GetString("The [color=orange]improper temperature level alert[/color] is blinking.\n"));
|
|
|
|
if(ImproperPressure)
|
|
message.AddMarkup(Loc.GetString("The [color=lightblue]improper environment pressure alert[/color] is blinking.\n"));
|
|
|
|
if(_missingGas > 0)
|
|
message.AddMarkup(Loc.GetString("The [color=cyan]improper gas environment alert[/color] is blinking.\n"));
|
|
}
|
|
}
|
|
}
|
|
}
|