Instead of each mutation being a flag that gets checked at some unique point in BotanySystem somewhere, they're now EntityEffects that get applied when the mutation occurs and when produce is harvested. One new list was added to SeedData so that multiple other fields could be removed. All the non-stat-change mutations that have been rolled are added to the Mutations list, and get applied to the plant when the mutation occurs or when a seed with the mutation is planted. Produce get mutations applied at harvest if they apply to the produce, and carry all of the plant's mutations over as a seed. This gets rid of the one-off checks for things like Slippery, Bioluminescent, Sentient, etc. The base odds of a mutation applying should be equal to the odds of the original mutation check. It pretended to have 1 bit flip (on averge) per mutation power, and odds of each mutation was the odds of one of its bit being flipped (1 /275 * bits). The 'thermometer code' applied for numbers will be replaced with simple random rolls, as both average out to the middle value. The new checks are much easier to understand and don't obfuscate the actual changes of something happening behind 3 layers of math. The biggest player-facing change is that Potency will be able to get over 65 significantly more often than it did in the previous system, but it will be just as common to get low values as high ones. Mutation definitions have been moved to a .yml file. These include the odds per tick per mutagen strength of that mutation applying that tick, the effect applied, if it applies to the plant and/or its produce. This makes mutations simpler to add and edit. This PR is limited specifically to the mutation logic. Improving other aspects of the system will be done in other PRs per the design document. Mutations was chosen first because its got the largest amount of one-off checks scattered all over that could be consolidated. Once this is merged, mutations could be contributed to the codebase with minimal extra work for later botany refactor PRs.
974 lines
35 KiB
C#
974 lines
35 KiB
C#
using Content.Server.Atmos.EntitySystems;
|
|
using Content.Server.Botany.Components;
|
|
using Content.Server.Fluids.Components;
|
|
using Content.Server.Kitchen.Components;
|
|
using Content.Server.Popups;
|
|
using Content.Shared.Chemistry.EntitySystems;
|
|
using Content.Shared.Atmos;
|
|
using Content.Shared.Botany;
|
|
using Content.Shared.Burial.Components;
|
|
using Content.Shared.Chemistry.Reagent;
|
|
using Content.Shared.Coordinates.Helpers;
|
|
using Content.Shared.Examine;
|
|
using Content.Shared.FixedPoint;
|
|
using Content.Shared.Hands.Components;
|
|
using Content.Shared.IdentityManagement;
|
|
using Content.Shared.Interaction;
|
|
using Content.Shared.Popups;
|
|
using Content.Shared.Random;
|
|
using Content.Shared.Tag;
|
|
using Robust.Server.GameObjects;
|
|
using Robust.Shared.Audio;
|
|
using Robust.Shared.Audio.Systems;
|
|
using Robust.Shared.Player;
|
|
using Robust.Shared.Prototypes;
|
|
using Robust.Shared.Random;
|
|
using Robust.Shared.Timing;
|
|
|
|
namespace Content.Server.Botany.Systems;
|
|
|
|
public sealed class PlantHolderSystem : EntitySystem
|
|
{
|
|
[Dependency] private readonly AtmosphereSystem _atmosphere = default!;
|
|
[Dependency] private readonly BotanySystem _botany = default!;
|
|
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
|
[Dependency] private readonly MutationSystem _mutation = default!;
|
|
[Dependency] private readonly AppearanceSystem _appearance = default!;
|
|
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
|
[Dependency] private readonly PopupSystem _popup = default!;
|
|
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
|
[Dependency] private readonly SharedPointLightSystem _pointLight = default!;
|
|
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
|
|
[Dependency] private readonly TagSystem _tagSystem = default!;
|
|
[Dependency] private readonly RandomHelperSystem _randomHelper = default!;
|
|
[Dependency] private readonly IRobustRandom _random = default!;
|
|
|
|
|
|
public const float HydroponicsSpeedMultiplier = 1f;
|
|
public const float HydroponicsConsumptionMultiplier = 2f;
|
|
|
|
public override void Initialize()
|
|
{
|
|
base.Initialize();
|
|
SubscribeLocalEvent<PlantHolderComponent, ExaminedEvent>(OnExamine);
|
|
SubscribeLocalEvent<PlantHolderComponent, InteractUsingEvent>(OnInteractUsing);
|
|
SubscribeLocalEvent<PlantHolderComponent, InteractHandEvent>(OnInteractHand);
|
|
}
|
|
|
|
public override void Update(float frameTime)
|
|
{
|
|
base.Update(frameTime);
|
|
|
|
var query = EntityQueryEnumerator<PlantHolderComponent>();
|
|
while (query.MoveNext(out var uid, out var plantHolder))
|
|
{
|
|
if (plantHolder.NextUpdate > _gameTiming.CurTime)
|
|
continue;
|
|
plantHolder.NextUpdate = _gameTiming.CurTime + plantHolder.UpdateDelay;
|
|
|
|
Update(uid, plantHolder);
|
|
}
|
|
}
|
|
|
|
private int GetCurrentGrowthStage(Entity<PlantHolderComponent> entity)
|
|
{
|
|
var (uid, component) = entity;
|
|
|
|
if (component.Seed == null)
|
|
return 0;
|
|
|
|
var result = Math.Max(1, (int)(component.Age * component.Seed.GrowthStages / component.Seed.Maturation));
|
|
return result;
|
|
}
|
|
|
|
private void OnExamine(Entity<PlantHolderComponent> entity, ref ExaminedEvent args)
|
|
{
|
|
if (!args.IsInDetailsRange)
|
|
return;
|
|
|
|
var (_, component) = entity;
|
|
|
|
using (args.PushGroup(nameof(PlantHolderComponent)))
|
|
{
|
|
if (component.Seed == null)
|
|
{
|
|
args.PushMarkup(Loc.GetString("plant-holder-component-nothing-planted-message"));
|
|
}
|
|
else if (!component.Dead)
|
|
{
|
|
var displayName = Loc.GetString(component.Seed.DisplayName);
|
|
args.PushMarkup(Loc.GetString("plant-holder-component-something-already-growing-message",
|
|
("seedName", displayName),
|
|
("toBeForm", displayName.EndsWith('s') ? "are" : "is")));
|
|
|
|
if (component.Health <= component.Seed.Endurance / 2)
|
|
{
|
|
args.PushMarkup(Loc.GetString(
|
|
"plant-holder-component-something-already-growing-low-health-message",
|
|
("healthState",
|
|
Loc.GetString(component.Age > component.Seed.Lifespan
|
|
? "plant-holder-component-plant-old-adjective"
|
|
: "plant-holder-component-plant-unhealthy-adjective"))));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
args.PushMarkup(Loc.GetString("plant-holder-component-dead-plant-matter-message"));
|
|
}
|
|
|
|
if (component.WeedLevel >= 5)
|
|
args.PushMarkup(Loc.GetString("plant-holder-component-weed-high-level-message"));
|
|
|
|
if (component.PestLevel >= 5)
|
|
args.PushMarkup(Loc.GetString("plant-holder-component-pest-high-level-message"));
|
|
|
|
args.PushMarkup(Loc.GetString($"plant-holder-component-water-level-message",
|
|
("waterLevel", (int)component.WaterLevel)));
|
|
args.PushMarkup(Loc.GetString($"plant-holder-component-nutrient-level-message",
|
|
("nutritionLevel", (int)component.NutritionLevel)));
|
|
|
|
if (component.DrawWarnings)
|
|
{
|
|
if (component.Toxins > 40f)
|
|
args.PushMarkup(Loc.GetString("plant-holder-component-toxins-high-warning"));
|
|
|
|
if (component.ImproperLight)
|
|
args.PushMarkup(Loc.GetString("plant-holder-component-light-improper-warning"));
|
|
|
|
if (component.ImproperHeat)
|
|
args.PushMarkup(Loc.GetString("plant-holder-component-heat-improper-warning"));
|
|
|
|
if (component.ImproperPressure)
|
|
args.PushMarkup(Loc.GetString("plant-holder-component-pressure-improper-warning"));
|
|
|
|
if (component.MissingGas > 0)
|
|
args.PushMarkup(Loc.GetString("plant-holder-component-gas-missing-warning"));
|
|
}
|
|
}
|
|
}
|
|
|
|
private void OnInteractUsing(Entity<PlantHolderComponent> entity, ref InteractUsingEvent args)
|
|
{
|
|
var (uid, component) = entity;
|
|
|
|
if (TryComp(args.Used, out SeedComponent? seeds))
|
|
{
|
|
if (component.Seed == null)
|
|
{
|
|
if (!_botany.TryGetSeed(seeds, out var seed))
|
|
return;
|
|
|
|
var name = Loc.GetString(seed.Name);
|
|
var noun = Loc.GetString(seed.Noun);
|
|
_popup.PopupCursor(Loc.GetString("plant-holder-component-plant-success-message",
|
|
("seedName", name),
|
|
("seedNoun", noun)), args.User, PopupType.Medium);
|
|
|
|
component.Seed = seed;
|
|
component.Dead = false;
|
|
component.Age = 1;
|
|
if (seeds.HealthOverride != null)
|
|
{
|
|
component.Health = seeds.HealthOverride.Value;
|
|
}
|
|
else
|
|
{
|
|
component.Health = component.Seed.Endurance;
|
|
}
|
|
component.LastCycle = _gameTiming.CurTime;
|
|
|
|
QueueDel(args.Used);
|
|
|
|
CheckLevelSanity(uid, component);
|
|
UpdateSprite(uid, component);
|
|
|
|
return;
|
|
}
|
|
|
|
_popup.PopupCursor(Loc.GetString("plant-holder-component-already-seeded-message",
|
|
("name", Comp<MetaDataComponent>(uid).EntityName)), args.User, PopupType.Medium);
|
|
return;
|
|
}
|
|
|
|
if (_tagSystem.HasTag(args.Used, "Hoe"))
|
|
{
|
|
if (component.WeedLevel > 0)
|
|
{
|
|
_popup.PopupCursor(Loc.GetString("plant-holder-component-remove-weeds-message",
|
|
("name", Comp<MetaDataComponent>(uid).EntityName)), args.User, PopupType.Medium);
|
|
_popup.PopupEntity(Loc.GetString("plant-holder-component-remove-weeds-others-message",
|
|
("otherName", Comp<MetaDataComponent>(args.User).EntityName)), uid, Filter.PvsExcept(args.User), true);
|
|
component.WeedLevel = 0;
|
|
UpdateSprite(uid, component);
|
|
}
|
|
else
|
|
{
|
|
_popup.PopupCursor(Loc.GetString("plant-holder-component-no-weeds-message"), args.User);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (HasComp<ShovelComponent>(args.Used))
|
|
{
|
|
if (component.Seed != null)
|
|
{
|
|
_popup.PopupCursor(Loc.GetString("plant-holder-component-remove-plant-message",
|
|
("name", Comp<MetaDataComponent>(uid).EntityName)), args.User, PopupType.Medium);
|
|
_popup.PopupEntity(Loc.GetString("plant-holder-component-remove-plant-others-message",
|
|
("name", Comp<MetaDataComponent>(args.User).EntityName)), uid, Filter.PvsExcept(args.User), true);
|
|
RemovePlant(uid, component);
|
|
}
|
|
else
|
|
{
|
|
_popup.PopupCursor(Loc.GetString("plant-holder-component-no-plant-message",
|
|
("name", Comp<MetaDataComponent>(uid).EntityName)), args.User);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (_solutionContainerSystem.TryGetDrainableSolution(args.Used, out var solution, out _)
|
|
&& _solutionContainerSystem.ResolveSolution(uid, component.SoilSolutionName, ref component.SoilSolution)
|
|
&& TryComp(args.Used, out SprayComponent? spray))
|
|
{
|
|
var amount = FixedPoint2.New(1);
|
|
|
|
var targetEntity = uid;
|
|
var solutionEntity = args.Used;
|
|
|
|
_audio.PlayPvs(spray.SpraySound, args.Used, AudioParams.Default.WithVariation(0.125f));
|
|
|
|
var split = _solutionContainerSystem.Drain(solutionEntity, solution.Value, amount);
|
|
|
|
if (split.Volume == 0)
|
|
{
|
|
_popup.PopupCursor(Loc.GetString("plant-holder-component-no-plant-message",
|
|
("owner", args.Used)), args.User);
|
|
return;
|
|
}
|
|
|
|
_popup.PopupCursor(Loc.GetString("plant-holder-component-spray-message",
|
|
("owner", uid),
|
|
("amount", split.Volume)), args.User, PopupType.Medium);
|
|
|
|
_solutionContainerSystem.TryAddSolution(component.SoilSolution.Value, split);
|
|
|
|
ForceUpdateByExternalCause(uid, component);
|
|
|
|
return;
|
|
}
|
|
|
|
if (_tagSystem.HasTag(args.Used, "PlantSampleTaker"))
|
|
{
|
|
if (component.Seed == null)
|
|
{
|
|
_popup.PopupCursor(Loc.GetString("plant-holder-component-nothing-to-sample-message"), args.User);
|
|
return;
|
|
}
|
|
|
|
if (component.Sampled)
|
|
{
|
|
_popup.PopupCursor(Loc.GetString("plant-holder-component-already-sampled-message"), args.User);
|
|
return;
|
|
}
|
|
|
|
if (component.Dead)
|
|
{
|
|
_popup.PopupCursor(Loc.GetString("plant-holder-component-dead-plant-message"), args.User);
|
|
return;
|
|
}
|
|
|
|
if (GetCurrentGrowthStage(entity) <= 1)
|
|
{
|
|
_popup.PopupCursor(Loc.GetString("plant-holder-component-early-sample-message"), args.User);
|
|
return;
|
|
}
|
|
|
|
component.Health -= (_random.Next(3, 5) * 10);
|
|
|
|
float? healthOverride;
|
|
if (component.Harvest)
|
|
{
|
|
healthOverride = null;
|
|
}
|
|
else
|
|
{
|
|
healthOverride = component.Health;
|
|
}
|
|
var packetSeed = component.Seed;
|
|
var seed = _botany.SpawnSeedPacket(packetSeed, Transform(args.User).Coordinates, args.User, healthOverride);
|
|
_randomHelper.RandomOffset(seed, 0.25f);
|
|
var displayName = Loc.GetString(component.Seed.DisplayName);
|
|
_popup.PopupCursor(Loc.GetString("plant-holder-component-take-sample-message",
|
|
("seedName", displayName)), args.User);
|
|
|
|
DoScream(entity.Owner, component.Seed);
|
|
|
|
if (_random.Prob(0.3f))
|
|
component.Sampled = true;
|
|
|
|
// Just in case.
|
|
CheckLevelSanity(uid, component);
|
|
ForceUpdateByExternalCause(uid, component);
|
|
|
|
return;
|
|
}
|
|
|
|
if (HasComp<SharpComponent>(args.Used))
|
|
DoHarvest(uid, args.User, component);
|
|
|
|
if (TryComp<ProduceComponent>(args.Used, out var produce))
|
|
{
|
|
_popup.PopupCursor(Loc.GetString("plant-holder-component-compost-message",
|
|
("owner", uid),
|
|
("usingItem", args.Used)), args.User, PopupType.Medium);
|
|
_popup.PopupEntity(Loc.GetString("plant-holder-component-compost-others-message",
|
|
("user", Identity.Entity(args.User, EntityManager)),
|
|
("usingItem", args.Used),
|
|
("owner", uid)), uid, Filter.PvsExcept(args.User), true);
|
|
|
|
if (_solutionContainerSystem.TryGetSolution(args.Used, produce.SolutionName, out var soln2, out var solution2))
|
|
{
|
|
if (_solutionContainerSystem.ResolveSolution(uid, component.SoilSolutionName, ref component.SoilSolution, out var solution1))
|
|
{
|
|
// We try to fit as much of the composted plant's contained solution into the hydroponics tray as we can,
|
|
// since the plant will be consumed anyway.
|
|
|
|
var fillAmount = FixedPoint2.Min(solution2.Volume, solution1.AvailableVolume);
|
|
_solutionContainerSystem.TryAddSolution(component.SoilSolution.Value, _solutionContainerSystem.SplitSolution(soln2.Value, fillAmount));
|
|
|
|
ForceUpdateByExternalCause(uid, component);
|
|
}
|
|
}
|
|
var seed = produce.Seed;
|
|
if (seed != null)
|
|
{
|
|
var nutrientBonus = seed.Potency / 2.5f;
|
|
AdjustNutrient(uid, nutrientBonus, component);
|
|
}
|
|
QueueDel(args.Used);
|
|
}
|
|
}
|
|
|
|
private void OnInteractHand(Entity<PlantHolderComponent> entity, ref InteractHandEvent args)
|
|
{
|
|
DoHarvest(entity, args.User, entity.Comp);
|
|
}
|
|
|
|
public void WeedInvasion()
|
|
{
|
|
// TODO
|
|
}
|
|
|
|
|
|
public void Update(EntityUid uid, PlantHolderComponent? component = null)
|
|
{
|
|
if (!Resolve(uid, ref component))
|
|
return;
|
|
|
|
UpdateReagents(uid, component);
|
|
|
|
var curTime = _gameTiming.CurTime;
|
|
|
|
if (component.ForceUpdate)
|
|
component.ForceUpdate = false;
|
|
else if (curTime < (component.LastCycle + component.CycleDelay))
|
|
{
|
|
if (component.UpdateSpriteAfterUpdate)
|
|
UpdateSprite(uid, component);
|
|
return;
|
|
}
|
|
|
|
component.LastCycle = curTime;
|
|
|
|
// Process mutations
|
|
if (component.MutationLevel > 0)
|
|
{
|
|
Mutate(uid, Math.Min(component.MutationLevel, 25), component);
|
|
component.UpdateSpriteAfterUpdate = true;
|
|
component.MutationLevel = 0;
|
|
}
|
|
|
|
// Weeds like water and nutrients! They may appear even if there's not a seed planted.
|
|
if (component.WaterLevel > 10 && component.NutritionLevel > 5)
|
|
{
|
|
var chance = 0f;
|
|
if (component.Seed == null)
|
|
chance = 0.05f;
|
|
else if (component.Seed.TurnIntoKudzu)
|
|
chance = 1f;
|
|
else
|
|
chance = 0.01f;
|
|
|
|
if (_random.Prob(chance))
|
|
component.WeedLevel += 1 + HydroponicsSpeedMultiplier * component.WeedCoefficient;
|
|
|
|
if (component.DrawWarnings)
|
|
component.UpdateSpriteAfterUpdate = true;
|
|
}
|
|
|
|
if (component.Seed != null && component.Seed.TurnIntoKudzu
|
|
&& component.WeedLevel >= component.Seed.WeedHighLevelThreshold)
|
|
{
|
|
Spawn(component.Seed.KudzuPrototype, Transform(uid).Coordinates.SnapToGrid(EntityManager));
|
|
component.Seed.TurnIntoKudzu = false;
|
|
component.Health = 0;
|
|
}
|
|
|
|
// There's a chance for a weed explosion to happen if weeds take over.
|
|
// Plants that are themselves weeds (WeedTolerance > 8) are unaffected.
|
|
if (component.WeedLevel >= 10 && _random.Prob(0.1f))
|
|
{
|
|
if (component.Seed == null || component.WeedLevel >= component.Seed.WeedTolerance + 2)
|
|
WeedInvasion();
|
|
}
|
|
|
|
// If we have no seed planted, or the plant is dead, stop processing here.
|
|
if (component.Seed == null || component.Dead)
|
|
{
|
|
if (component.UpdateSpriteAfterUpdate)
|
|
UpdateSprite(uid, component);
|
|
|
|
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))
|
|
{
|
|
component.PestLevel += 0.5f * HydroponicsSpeedMultiplier;
|
|
if (component.DrawWarnings)
|
|
component.UpdateSpriteAfterUpdate = true;
|
|
}
|
|
|
|
// Advance plant age here.
|
|
if (component.SkipAging > 0)
|
|
component.SkipAging--;
|
|
else
|
|
{
|
|
if (_random.Prob(0.8f))
|
|
component.Age += (int)(1 * HydroponicsSpeedMultiplier);
|
|
|
|
component.UpdateSpriteAfterUpdate = true;
|
|
}
|
|
|
|
// Nutrient consumption.
|
|
if (component.Seed.NutrientConsumption > 0 && component.NutritionLevel > 0 && _random.Prob(0.75f))
|
|
{
|
|
component.NutritionLevel -= MathF.Max(0f, component.Seed.NutrientConsumption * HydroponicsSpeedMultiplier);
|
|
if (component.DrawWarnings)
|
|
component.UpdateSpriteAfterUpdate = true;
|
|
}
|
|
|
|
// Water consumption.
|
|
if (component.Seed.WaterConsumption > 0 && component.WaterLevel > 0 && _random.Prob(0.75f))
|
|
{
|
|
component.WaterLevel -= MathF.Max(0f,
|
|
component.Seed.WaterConsumption * HydroponicsConsumptionMultiplier * HydroponicsSpeedMultiplier);
|
|
if (component.DrawWarnings)
|
|
component.UpdateSpriteAfterUpdate = true;
|
|
}
|
|
|
|
var healthMod = _random.Next(1, 3) * HydroponicsSpeedMultiplier;
|
|
|
|
// Make sure genetics are viable.
|
|
if (!component.Seed.Viable)
|
|
{
|
|
AffectGrowth(uid, -1, component);
|
|
component.Health -= 6 * healthMod;
|
|
}
|
|
|
|
// Prevents the plant from aging when lacking resources.
|
|
// Limits the effect on aging so that when resources are added, the plant starts growing in a reasonable amount of time.
|
|
if (component.SkipAging < 10)
|
|
{
|
|
// Make sure the plant is not starving.
|
|
if (component.NutritionLevel > 5)
|
|
{
|
|
component.Health += Convert.ToInt32(_random.Prob(0.35f)) * healthMod;
|
|
}
|
|
else
|
|
{
|
|
AffectGrowth(uid, -1, component);
|
|
component.Health -= healthMod;
|
|
}
|
|
|
|
// Make sure the plant is not thirsty.
|
|
if (component.WaterLevel > 10)
|
|
{
|
|
component.Health += Convert.ToInt32(_random.Prob(0.35f)) * healthMod;
|
|
}
|
|
else
|
|
{
|
|
AffectGrowth(uid, -1, component);
|
|
component.Health -= healthMod;
|
|
}
|
|
|
|
if (component.DrawWarnings)
|
|
component.UpdateSpriteAfterUpdate = true;
|
|
}
|
|
|
|
var environment = _atmosphere.GetContainingMixture(uid, true, true) ?? GasMixture.SpaceGas;
|
|
|
|
component.MissingGas = 0;
|
|
if (component.Seed.ConsumeGasses.Count > 0)
|
|
{
|
|
foreach (var (gas, amount) in component.Seed.ConsumeGasses)
|
|
{
|
|
if (environment.GetMoles(gas) < amount)
|
|
{
|
|
component.MissingGas++;
|
|
continue;
|
|
}
|
|
|
|
environment.AdjustMoles(gas, -amount);
|
|
}
|
|
|
|
if (component.MissingGas > 0)
|
|
{
|
|
component.Health -= component.MissingGas * HydroponicsSpeedMultiplier;
|
|
if (component.DrawWarnings)
|
|
component.UpdateSpriteAfterUpdate = true;
|
|
}
|
|
}
|
|
|
|
// SeedPrototype pressure resistance.
|
|
var pressure = environment.Pressure;
|
|
if (pressure < component.Seed.LowPressureTolerance || pressure > component.Seed.HighPressureTolerance)
|
|
{
|
|
component.Health -= healthMod;
|
|
component.ImproperPressure = true;
|
|
if (component.DrawWarnings)
|
|
component.UpdateSpriteAfterUpdate = true;
|
|
}
|
|
else
|
|
{
|
|
component.ImproperPressure = false;
|
|
}
|
|
|
|
// SeedPrototype ideal temperature.
|
|
if (MathF.Abs(environment.Temperature - component.Seed.IdealHeat) > component.Seed.HeatTolerance)
|
|
{
|
|
component.Health -= healthMod;
|
|
component.ImproperHeat = true;
|
|
if (component.DrawWarnings)
|
|
component.UpdateSpriteAfterUpdate = true;
|
|
}
|
|
else
|
|
{
|
|
component.ImproperHeat = false;
|
|
}
|
|
|
|
// Gas production.
|
|
var exudeCount = component.Seed.ExudeGasses.Count;
|
|
if (exudeCount > 0)
|
|
{
|
|
foreach (var (gas, amount) in component.Seed.ExudeGasses)
|
|
{
|
|
environment.AdjustMoles(gas,
|
|
MathF.Max(1f, MathF.Round(amount * MathF.Round(component.Seed.Potency) / exudeCount)));
|
|
}
|
|
}
|
|
|
|
// Toxin levels beyond the plant's tolerance cause damage.
|
|
// They are, however, slowly reduced over time.
|
|
if (component.Toxins > 0)
|
|
{
|
|
var toxinUptake = MathF.Max(1, MathF.Round(component.Toxins / 10f));
|
|
if (component.Toxins > component.Seed.ToxinsTolerance)
|
|
{
|
|
component.Health -= toxinUptake;
|
|
}
|
|
|
|
component.Toxins -= toxinUptake;
|
|
if (component.DrawWarnings)
|
|
component.UpdateSpriteAfterUpdate = true;
|
|
}
|
|
|
|
// Weed levels.
|
|
if (component.PestLevel > 0)
|
|
{
|
|
// TODO: Carnivorous plants?
|
|
if (component.PestLevel > component.Seed.PestTolerance)
|
|
{
|
|
component.Health -= HydroponicsSpeedMultiplier;
|
|
}
|
|
|
|
if (component.DrawWarnings)
|
|
component.UpdateSpriteAfterUpdate = true;
|
|
}
|
|
|
|
// Weed levels.
|
|
if (component.WeedLevel > 0)
|
|
{
|
|
// TODO: Parasitic plants.
|
|
if (component.WeedLevel >= component.Seed.WeedTolerance)
|
|
{
|
|
component.Health -= HydroponicsSpeedMultiplier;
|
|
}
|
|
|
|
if (component.DrawWarnings)
|
|
component.UpdateSpriteAfterUpdate = true;
|
|
}
|
|
|
|
if (component.Age > component.Seed.Lifespan)
|
|
{
|
|
component.Health -= _random.Next(3, 5) * HydroponicsSpeedMultiplier;
|
|
if (component.DrawWarnings)
|
|
component.UpdateSpriteAfterUpdate = true;
|
|
}
|
|
else if (component.Age < 0) // Revert back to seed packet!
|
|
{
|
|
var packetSeed = component.Seed;
|
|
// will put it in the trays hands if it has any, please do not try doing this
|
|
_botany.SpawnSeedPacket(packetSeed, Transform(uid).Coordinates, uid);
|
|
RemovePlant(uid, component);
|
|
component.ForceUpdate = true;
|
|
Update(uid, component);
|
|
return;
|
|
}
|
|
|
|
CheckHealth(uid, component);
|
|
|
|
if (component.Harvest && component.Seed.HarvestRepeat == HarvestType.SelfHarvest)
|
|
AutoHarvest(uid, component);
|
|
|
|
// If enough time has passed since the plant was harvested, we're ready to harvest again!
|
|
if (!component.Dead && component.Seed.ProductPrototypes.Count > 0)
|
|
{
|
|
if (component.Age > component.Seed.Production)
|
|
{
|
|
if (component.Age - component.LastProduce > component.Seed.Production && !component.Harvest)
|
|
{
|
|
component.Harvest = true;
|
|
component.LastProduce = component.Age;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (component.Harvest)
|
|
{
|
|
component.Harvest = false;
|
|
component.LastProduce = component.Age;
|
|
}
|
|
}
|
|
}
|
|
|
|
CheckLevelSanity(uid, component);
|
|
|
|
if (component.UpdateSpriteAfterUpdate)
|
|
UpdateSprite(uid, component);
|
|
}
|
|
|
|
//TODO: kill this bullshit
|
|
public void CheckLevelSanity(EntityUid uid, PlantHolderComponent? component = null)
|
|
{
|
|
if (!Resolve(uid, ref component))
|
|
return;
|
|
|
|
if (component.Seed != null)
|
|
component.Health = MathHelper.Clamp(component.Health, 0, component.Seed.Endurance);
|
|
else
|
|
{
|
|
component.Health = 0f;
|
|
component.Dead = false;
|
|
}
|
|
|
|
component.MutationLevel = MathHelper.Clamp(component.MutationLevel, 0f, 100f);
|
|
component.NutritionLevel = MathHelper.Clamp(component.NutritionLevel, 0f, 100f);
|
|
component.WaterLevel = MathHelper.Clamp(component.WaterLevel, 0f, 100f);
|
|
component.PestLevel = MathHelper.Clamp(component.PestLevel, 0f, 10f);
|
|
component.WeedLevel = MathHelper.Clamp(component.WeedLevel, 0f, 10f);
|
|
component.Toxins = MathHelper.Clamp(component.Toxins, 0f, 100f);
|
|
component.YieldMod = MathHelper.Clamp(component.YieldMod, 0, 2);
|
|
component.MutationMod = MathHelper.Clamp(component.MutationMod, 0f, 3f);
|
|
}
|
|
|
|
public bool DoHarvest(EntityUid plantholder, EntityUid user, PlantHolderComponent? component = null)
|
|
{
|
|
if (!Resolve(plantholder, ref component))
|
|
return false;
|
|
|
|
if (component.Seed == null || Deleted(user))
|
|
return false;
|
|
|
|
|
|
if (component.Harvest && !component.Dead)
|
|
{
|
|
if (TryComp<HandsComponent>(user, out var hands))
|
|
{
|
|
if (!_botany.CanHarvest(component.Seed, hands.ActiveHandEntity))
|
|
return false;
|
|
}
|
|
else if (!_botany.CanHarvest(component.Seed))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
_botany.Harvest(component.Seed, user, component.YieldMod);
|
|
AfterHarvest(plantholder, component);
|
|
return true;
|
|
}
|
|
|
|
if (!component.Dead)
|
|
return false;
|
|
|
|
RemovePlant(plantholder, component);
|
|
AfterHarvest(plantholder, component);
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Force do scream on PlantHolder (like plant is screaming) using seed's ScreamSound specifier (collection or soundPath)
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public bool DoScream(EntityUid plantholder, SeedData? seed = null)
|
|
{
|
|
if (seed == null || seed.CanScream == false)
|
|
return false;
|
|
|
|
_audio.PlayPvs(seed.ScreamSound, plantholder);
|
|
return true;
|
|
}
|
|
|
|
public void AutoHarvest(EntityUid uid, PlantHolderComponent? component = null)
|
|
{
|
|
if (!Resolve(uid, ref component))
|
|
return;
|
|
|
|
if (component.Seed == null || !component.Harvest)
|
|
return;
|
|
|
|
_botany.AutoHarvest(component.Seed, Transform(uid).Coordinates);
|
|
AfterHarvest(uid, component);
|
|
}
|
|
|
|
private void AfterHarvest(EntityUid uid, PlantHolderComponent? component = null)
|
|
{
|
|
if (!Resolve(uid, ref component))
|
|
return;
|
|
|
|
component.Harvest = false;
|
|
component.LastProduce = component.Age;
|
|
|
|
DoScream(uid, component.Seed);
|
|
|
|
if (component.Seed?.HarvestRepeat == HarvestType.NoRepeat)
|
|
RemovePlant(uid, component);
|
|
|
|
CheckLevelSanity(uid, component);
|
|
UpdateSprite(uid, component);
|
|
}
|
|
|
|
public void CheckHealth(EntityUid uid, PlantHolderComponent? component = null)
|
|
{
|
|
if (!Resolve(uid, ref component))
|
|
return;
|
|
|
|
if (component.Health <= 0)
|
|
{
|
|
Die(uid, component);
|
|
}
|
|
}
|
|
|
|
public void Die(EntityUid uid, PlantHolderComponent? component = null)
|
|
{
|
|
if (!Resolve(uid, ref component))
|
|
return;
|
|
|
|
component.Dead = true;
|
|
component.Harvest = false;
|
|
component.MutationLevel = 0;
|
|
component.YieldMod = 1;
|
|
component.MutationMod = 1;
|
|
component.ImproperLight = false;
|
|
component.ImproperHeat = false;
|
|
component.ImproperPressure = false;
|
|
component.WeedLevel += 1 * HydroponicsSpeedMultiplier;
|
|
component.PestLevel = 0;
|
|
UpdateSprite(uid, component);
|
|
}
|
|
|
|
public void RemovePlant(EntityUid uid, PlantHolderComponent? component = null)
|
|
{
|
|
if (!Resolve(uid, ref component))
|
|
return;
|
|
|
|
component.YieldMod = 1;
|
|
component.MutationMod = 1;
|
|
component.PestLevel = 0;
|
|
component.Seed = null;
|
|
component.Dead = false;
|
|
component.Age = 0;
|
|
component.LastProduce = 0;
|
|
component.Sampled = false;
|
|
component.Harvest = false;
|
|
component.ImproperLight = false;
|
|
component.ImproperPressure = false;
|
|
component.ImproperHeat = false;
|
|
|
|
UpdateSprite(uid, component);
|
|
}
|
|
|
|
public void AffectGrowth(EntityUid uid, int amount, PlantHolderComponent? component = null)
|
|
{
|
|
if (!Resolve(uid, ref component))
|
|
return;
|
|
|
|
if (component.Seed == null)
|
|
return;
|
|
|
|
if (amount > 0)
|
|
{
|
|
if (component.Age < component.Seed.Maturation)
|
|
component.Age += amount;
|
|
else if (!component.Harvest && component.Seed.Yield <= 0f)
|
|
component.LastProduce -= amount;
|
|
}
|
|
else
|
|
{
|
|
if (component.Age < component.Seed.Maturation)
|
|
component.SkipAging++;
|
|
else if (!component.Harvest && component.Seed.Yield <= 0f)
|
|
component.LastProduce += amount;
|
|
}
|
|
}
|
|
|
|
public void AdjustNutrient(EntityUid uid, float amount, PlantHolderComponent? component = null)
|
|
{
|
|
if (!Resolve(uid, ref component))
|
|
return;
|
|
|
|
component.NutritionLevel += amount;
|
|
}
|
|
|
|
public void AdjustWater(EntityUid uid, float amount, PlantHolderComponent? component = null)
|
|
{
|
|
if (!Resolve(uid, ref component))
|
|
return;
|
|
|
|
component.WaterLevel += amount;
|
|
|
|
// Water dilutes toxins.
|
|
if (amount > 0)
|
|
{
|
|
component.Toxins -= amount * 4f;
|
|
}
|
|
}
|
|
|
|
public void UpdateReagents(EntityUid uid, PlantHolderComponent? component = null)
|
|
{
|
|
if (!Resolve(uid, ref component))
|
|
return;
|
|
|
|
if (!_solutionContainerSystem.ResolveSolution(uid, component.SoilSolutionName, ref component.SoilSolution, out var solution))
|
|
return;
|
|
|
|
if (solution.Volume > 0 && component.MutationLevel < 25)
|
|
{
|
|
var amt = FixedPoint2.New(1);
|
|
foreach (var entry in _solutionContainerSystem.RemoveEachReagent(component.SoilSolution.Value, amt))
|
|
{
|
|
var reagentProto = _prototype.Index<ReagentPrototype>(entry.Reagent.Prototype);
|
|
reagentProto.ReactionPlant(uid, entry, solution);
|
|
}
|
|
}
|
|
|
|
CheckLevelSanity(uid, component);
|
|
}
|
|
|
|
private void Mutate(EntityUid uid, float severity, PlantHolderComponent? component = null)
|
|
{
|
|
if (!Resolve(uid, ref component))
|
|
return;
|
|
|
|
if (component.Seed != null)
|
|
{
|
|
EnsureUniqueSeed(uid, component);
|
|
_mutation.MutateSeed(uid, ref component.Seed, severity);
|
|
}
|
|
}
|
|
|
|
public void UpdateSprite(EntityUid uid, PlantHolderComponent? component = null)
|
|
{
|
|
if (!Resolve(uid, ref component))
|
|
return;
|
|
|
|
component.UpdateSpriteAfterUpdate = false;
|
|
|
|
if (!TryComp<AppearanceComponent>(uid, out var app))
|
|
return;
|
|
|
|
if (component.Seed != null)
|
|
{
|
|
if (component.DrawWarnings)
|
|
{
|
|
_appearance.SetData(uid, PlantHolderVisuals.HealthLight, component.Health <= component.Seed.Endurance / 2f);
|
|
}
|
|
|
|
if (component.Dead)
|
|
{
|
|
_appearance.SetData(uid, PlantHolderVisuals.PlantRsi, component.Seed.PlantRsi.ToString(), app);
|
|
_appearance.SetData(uid, PlantHolderVisuals.PlantState, "dead", app);
|
|
}
|
|
else if (component.Harvest)
|
|
{
|
|
_appearance.SetData(uid, PlantHolderVisuals.PlantRsi, component.Seed.PlantRsi.ToString(), app);
|
|
_appearance.SetData(uid, PlantHolderVisuals.PlantState, "harvest", app);
|
|
}
|
|
else if (component.Age < component.Seed.Maturation)
|
|
{
|
|
var growthStage = GetCurrentGrowthStage((uid, component));
|
|
|
|
_appearance.SetData(uid, PlantHolderVisuals.PlantRsi, component.Seed.PlantRsi.ToString(), app);
|
|
_appearance.SetData(uid, PlantHolderVisuals.PlantState, $"stage-{growthStage}", app);
|
|
component.LastProduce = component.Age;
|
|
}
|
|
else
|
|
{
|
|
_appearance.SetData(uid, PlantHolderVisuals.PlantRsi, component.Seed.PlantRsi.ToString(), app);
|
|
_appearance.SetData(uid, PlantHolderVisuals.PlantState, $"stage-{component.Seed.GrowthStages}", app);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
_appearance.SetData(uid, PlantHolderVisuals.PlantState, "", app);
|
|
_appearance.SetData(uid, PlantHolderVisuals.HealthLight, false, app);
|
|
}
|
|
|
|
if (!component.DrawWarnings)
|
|
return;
|
|
|
|
_appearance.SetData(uid, PlantHolderVisuals.WaterLight, component.WaterLevel <= 15, app);
|
|
_appearance.SetData(uid, PlantHolderVisuals.NutritionLight, component.NutritionLevel <= 8, app);
|
|
_appearance.SetData(uid, PlantHolderVisuals.AlertLight,
|
|
component.WeedLevel >= 5 || component.PestLevel >= 5 || component.Toxins >= 40 || component.ImproperHeat ||
|
|
component.ImproperLight || component.ImproperPressure || component.MissingGas > 0, app);
|
|
_appearance.SetData(uid, PlantHolderVisuals.HarvestLight, component.Harvest, app);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check if the currently contained seed is unique. If it is not, clone it so that we have a unique seed.
|
|
/// Necessary to avoid modifying global seeds.
|
|
/// </summary>
|
|
public void EnsureUniqueSeed(EntityUid uid, PlantHolderComponent? component = null)
|
|
{
|
|
if (!Resolve(uid, ref component))
|
|
return;
|
|
|
|
if (component.Seed is { Unique: false })
|
|
component.Seed = component.Seed.Clone();
|
|
}
|
|
|
|
public void ForceUpdateByExternalCause(EntityUid uid, PlantHolderComponent? component = null)
|
|
{
|
|
if (!Resolve(uid, ref component))
|
|
return;
|
|
|
|
component.SkipAging++; // We're forcing an update cycle, so one age hasn't passed.
|
|
component.ForceUpdate = true;
|
|
Update(uid, component);
|
|
}
|
|
}
|