diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 14f591ec87..defe08ef23 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -28,6 +28,7 @@
/Content.*/Stunnable/ @Princess-Cheeseballs
/Content.*/Nutrition/ @Princess-Cheeseballs
+/Content.*/EntityEffects @Princess-Cheeseballs @sowelipililimute
# SKREEEE
/Content.*.Database/ @PJB3005 @DrSmugleaf
diff --git a/Content.Client/Temperature/Systems/TemperatureSystem.cs b/Content.Client/Temperature/Systems/TemperatureSystem.cs
new file mode 100644
index 0000000000..94a1e836e8
--- /dev/null
+++ b/Content.Client/Temperature/Systems/TemperatureSystem.cs
@@ -0,0 +1,8 @@
+using Content.Shared.Temperature.Systems;
+
+namespace Content.Client.Temperature.Systems;
+
+///
+/// This exists so runs on client/>
+///
+public sealed class TemperatureSystem : SharedTemperatureSystem;
diff --git a/Content.Server/Atmos/EntitySystems/FlammableSystem.cs b/Content.Server/Atmos/EntitySystems/FlammableSystem.cs
index 424f52ed58..81bd4e5c6c 100644
--- a/Content.Server/Atmos/EntitySystems/FlammableSystem.cs
+++ b/Content.Server/Atmos/EntitySystems/FlammableSystem.cs
@@ -1,7 +1,6 @@
using Content.Server.Administration.Logs;
using Content.Server.Atmos.Components;
using Content.Server.Stunnable;
-using Content.Server.Temperature.Components;
using Content.Server.Temperature.Systems;
using Content.Server.Damage.Components;
using Content.Shared.ActionBlocker;
@@ -24,6 +23,7 @@ using Content.Shared.Toggleable;
using Content.Shared.Weapons.Melee.Events;
using Content.Shared.FixedPoint;
using Content.Shared.Hands;
+using Content.Shared.Temperature.Components;
using Robust.Server.Audio;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Events;
diff --git a/Content.Server/Atmos/Rotting/RottingSystem.cs b/Content.Server/Atmos/Rotting/RottingSystem.cs
index 6f14debc3d..57c1504b16 100644
--- a/Content.Server/Atmos/Rotting/RottingSystem.cs
+++ b/Content.Server/Atmos/Rotting/RottingSystem.cs
@@ -1,9 +1,9 @@
using Content.Server.Atmos.EntitySystems;
-using Content.Server.Temperature.Components;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Rotting;
using Content.Shared.Body.Events;
using Content.Shared.Damage;
+using Content.Shared.Temperature.Components;
using Robust.Server.Containers;
using Robust.Shared.Physics.Components;
using Robust.Shared.Timing;
diff --git a/Content.Server/Body/Components/MetabolizerComponent.cs b/Content.Server/Body/Components/MetabolizerComponent.cs
index 5821b0d944..46d2fdd8e8 100644
--- a/Content.Server/Body/Components/MetabolizerComponent.cs
+++ b/Content.Server/Body/Components/MetabolizerComponent.cs
@@ -10,13 +10,13 @@ namespace Content.Server.Body.Components
///
/// Handles metabolizing various reagents with given effects.
///
- [RegisterComponent, Access(typeof(MetabolizerSystem))]
+ [RegisterComponent, AutoGenerateComponentPause, Access(typeof(MetabolizerSystem))]
public sealed partial class MetabolizerComponent : Component
{
///
/// The next time that reagents will be metabolized.
///
- [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
+ [DataField, AutoPausedField]
public TimeSpan NextUpdate;
///
diff --git a/Content.Server/Body/Systems/MetabolizerSystem.cs b/Content.Server/Body/Systems/MetabolizerSystem.cs
index c59f87f576..6679bfea54 100644
--- a/Content.Server/Body/Systems/MetabolizerSystem.cs
+++ b/Content.Server/Body/Systems/MetabolizerSystem.cs
@@ -1,14 +1,18 @@
using Content.Server.Body.Components;
-using Content.Shared.Administration.Logs;
using Content.Shared.Body.Events;
using Content.Shared.Body.Organ;
+using Content.Shared.Body.Prototypes;
using Content.Shared.Body.Systems;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reagent;
-using Content.Shared.Database;
+using Content.Shared.EntityConditions;
+using Content.Shared.EntityConditions.Conditions;
+using Content.Shared.EntityConditions.Conditions.Body;
using Content.Shared.EntityEffects;
+using Content.Shared.EntityEffects.Effects.Body;
+using Content.Shared.EntityEffects.Effects.Solution;
using Content.Shared.FixedPoint;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
@@ -17,210 +21,258 @@ using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
-namespace Content.Server.Body.Systems
+namespace Content.Server.Body.Systems;
+
+///
+public sealed class MetabolizerSystem : SharedMetabolizerSystem
{
- ///
- public sealed class MetabolizerSystem : SharedMetabolizerSystem
+ [Dependency] private readonly IGameTiming _gameTiming = default!;
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly MobStateSystem _mobStateSystem = default!;
+ [Dependency] private readonly SharedEntityConditionsSystem _entityConditions = default!;
+ [Dependency] private readonly SharedEntityEffectsSystem _entityEffects = default!;
+ [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
+
+ private EntityQuery _organQuery;
+ private EntityQuery _solutionQuery;
+ private static readonly ProtoId Gas = "Gas";
+
+ public override void Initialize()
{
- [Dependency] private readonly IGameTiming _gameTiming = default!;
- [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
- [Dependency] private readonly IRobustRandom _random = default!;
- [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
- [Dependency] private readonly MobStateSystem _mobStateSystem = default!;
- [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
+ base.Initialize();
- private EntityQuery _organQuery;
- private EntityQuery _solutionQuery;
+ _organQuery = GetEntityQuery();
+ _solutionQuery = GetEntityQuery();
- public override void Initialize()
+ SubscribeLocalEvent(OnMetabolizerInit);
+ SubscribeLocalEvent(OnMapInit);
+ SubscribeLocalEvent(OnApplyMetabolicMultiplier);
+ }
+
+ private void OnMapInit(Entity ent, ref MapInitEvent args)
+ {
+ ent.Comp.NextUpdate = _gameTiming.CurTime + ent.Comp.AdjustedUpdateInterval;
+ }
+
+ private void OnMetabolizerInit(Entity entity, ref ComponentInit args)
+ {
+ if (!entity.Comp.SolutionOnBody)
{
- base.Initialize();
-
- _organQuery = GetEntityQuery();
- _solutionQuery = GetEntityQuery();
-
- SubscribeLocalEvent(OnMetabolizerInit);
- SubscribeLocalEvent(OnMapInit);
- SubscribeLocalEvent(OnUnpaused);
- SubscribeLocalEvent(OnApplyMetabolicMultiplier);
+ _solutionContainerSystem.EnsureSolution(entity.Owner, entity.Comp.SolutionName, out _);
}
-
- private void OnMapInit(Entity ent, ref MapInitEvent args)
+ else if (_organQuery.CompOrNull(entity)?.Body is { } body)
{
- ent.Comp.NextUpdate = _gameTiming.CurTime + ent.Comp.AdjustedUpdateInterval;
- }
-
- private void OnUnpaused(Entity ent, ref EntityUnpausedEvent args)
- {
- ent.Comp.NextUpdate += args.PausedTime;
- }
-
- private void OnMetabolizerInit(Entity entity, ref ComponentInit args)
- {
- if (!entity.Comp.SolutionOnBody)
- {
- _solutionContainerSystem.EnsureSolution(entity.Owner, entity.Comp.SolutionName, out _);
- }
- else if (_organQuery.CompOrNull(entity)?.Body is { } body)
- {
- _solutionContainerSystem.EnsureSolution(body, entity.Comp.SolutionName, out _);
- }
- }
-
- private void OnApplyMetabolicMultiplier(Entity ent, ref ApplyMetabolicMultiplierEvent args)
- {
- ent.Comp.UpdateIntervalMultiplier = args.Multiplier;
- }
-
- public override void Update(float frameTime)
- {
- base.Update(frameTime);
-
- var metabolizers = new ValueList<(EntityUid Uid, MetabolizerComponent Component)>(Count());
- var query = EntityQueryEnumerator();
-
- while (query.MoveNext(out var uid, out var comp))
- {
- metabolizers.Add((uid, comp));
- }
-
- foreach (var (uid, metab) in metabolizers)
- {
- // Only update as frequently as it should
- if (_gameTiming.CurTime < metab.NextUpdate)
- continue;
-
- metab.NextUpdate += metab.AdjustedUpdateInterval;
- TryMetabolize((uid, metab));
- }
- }
-
- private void TryMetabolize(Entity ent)
- {
- _organQuery.Resolve(ent, ref ent.Comp2, logMissing: false);
-
- // First step is get the solution we actually care about
- var solutionName = ent.Comp1.SolutionName;
- Solution? solution = null;
- Entity? soln = default!;
- EntityUid? solutionEntityUid = null;
-
- if (ent.Comp1.SolutionOnBody)
- {
- if (ent.Comp2?.Body is { } body)
- {
- if (!_solutionQuery.Resolve(body, ref ent.Comp3, logMissing: false))
- return;
-
- _solutionContainerSystem.TryGetSolution((body, ent.Comp3), solutionName, out soln, out solution);
- solutionEntityUid = body;
- }
- }
- else
- {
- if (!_solutionQuery.Resolve(ent, ref ent.Comp3, logMissing: false))
- return;
-
- _solutionContainerSystem.TryGetSolution((ent, ent), solutionName, out soln, out solution);
- solutionEntityUid = ent;
- }
-
- if (solutionEntityUid is null
- || soln is null
- || solution is null
- || solution.Contents.Count == 0)
- {
- return;
- }
-
- // randomize the reagent list so we don't have any weird quirks
- // like alphabetical order or insertion order mattering for processing
- var list = solution.Contents.ToArray();
- _random.Shuffle(list);
-
- int reagents = 0;
- foreach (var (reagent, quantity) in list)
- {
- if (!_prototypeManager.TryIndex(reagent.Prototype, out var proto))
- continue;
-
- var mostToRemove = FixedPoint2.Zero;
- if (proto.Metabolisms is null)
- {
- if (ent.Comp1.RemoveEmpty)
- {
- solution.RemoveReagent(reagent, FixedPoint2.New(1));
- }
-
- continue;
- }
-
- // we're done here entirely if this is true
- if (reagents >= ent.Comp1.MaxReagentsProcessable)
- return;
-
-
- // loop over all our groups and see which ones apply
- if (ent.Comp1.MetabolismGroups is null)
- continue;
-
- foreach (var group in ent.Comp1.MetabolismGroups)
- {
- if (!proto.Metabolisms.TryGetValue(group.Id, out var entry))
- continue;
-
- var rate = entry.MetabolismRate * group.MetabolismRateModifier;
-
- // Remove $rate, as long as there's enough reagent there to actually remove that much
- mostToRemove = FixedPoint2.Clamp(rate, 0, quantity);
-
- float scale = (float) mostToRemove / (float) rate;
-
- // if it's possible for them to be dead, and they are,
- // then we shouldn't process any effects, but should probably
- // still remove reagents
- if (TryComp(solutionEntityUid.Value, out var state))
- {
- if (!proto.WorksOnTheDead && _mobStateSystem.IsDead(solutionEntityUid.Value, state))
- continue;
- }
-
- var actualEntity = ent.Comp2?.Body ?? solutionEntityUid.Value;
- var args = new EntityEffectReagentArgs(actualEntity, EntityManager, ent, solution, mostToRemove, proto, null, scale);
-
- // do all effects, if conditions apply
- foreach (var effect in entry.Effects)
- {
- if (!effect.ShouldApply(args, _random))
- continue;
-
- if (effect.ShouldLog)
- {
- _adminLogger.Add(
- LogType.ReagentEffect,
- effect.LogImpact,
- $"Metabolism effect {effect.GetType().Name:effect}"
- + $" of reagent {proto.LocalizedName:reagent}"
- + $" applied on entity {actualEntity:entity}"
- + $" at {Transform(actualEntity).Coordinates:coordinates}"
- );
- }
-
- effect.Effect(args);
- }
- }
-
- // remove a certain amount of reagent
- if (mostToRemove > FixedPoint2.Zero)
- {
- solution.RemoveReagent(reagent, mostToRemove);
-
- // We have processed a reagant, so count it towards the cap
- reagents += 1;
- }
- }
-
- _solutionContainerSystem.UpdateChemicals(soln.Value);
+ _solutionContainerSystem.EnsureSolution(body, entity.Comp.SolutionName, out _);
}
}
+
+ private void OnApplyMetabolicMultiplier(Entity ent, ref ApplyMetabolicMultiplierEvent args)
+ {
+ ent.Comp.UpdateIntervalMultiplier = args.Multiplier;
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ var metabolizers = new ValueList<(EntityUid Uid, MetabolizerComponent Component)>(Count());
+ var query = EntityQueryEnumerator();
+
+ while (query.MoveNext(out var uid, out var comp))
+ {
+ metabolizers.Add((uid, comp));
+ }
+
+ foreach (var (uid, metab) in metabolizers)
+ {
+ // Only update as frequently as it should
+ if (_gameTiming.CurTime < metab.NextUpdate)
+ continue;
+
+ metab.NextUpdate += metab.AdjustedUpdateInterval;
+ TryMetabolize((uid, metab));
+ }
+ }
+
+ private void TryMetabolize(Entity ent)
+ {
+ _organQuery.Resolve(ent, ref ent.Comp2, logMissing: false);
+
+ // First step is get the solution we actually care about
+ var solutionName = ent.Comp1.SolutionName;
+ Solution? solution = null;
+ Entity? soln = default!;
+ EntityUid? solutionEntityUid = null;
+
+ if (ent.Comp1.SolutionOnBody)
+ {
+ if (ent.Comp2?.Body is { } body)
+ {
+ if (!_solutionQuery.Resolve(body, ref ent.Comp3, logMissing: false))
+ return;
+
+ _solutionContainerSystem.TryGetSolution((body, ent.Comp3), solutionName, out soln, out solution);
+ solutionEntityUid = body;
+ }
+ }
+ else
+ {
+ if (!_solutionQuery.Resolve(ent, ref ent.Comp3, logMissing: false))
+ return;
+
+ _solutionContainerSystem.TryGetSolution((ent, ent), solutionName, out soln, out solution);
+ solutionEntityUid = ent;
+ }
+
+ if (solutionEntityUid is null
+ || soln is null
+ || solution is null
+ || solution.Contents.Count == 0)
+ {
+ return;
+ }
+
+ // randomize the reagent list so we don't have any weird quirks
+ // like alphabetical order or insertion order mattering for processing
+ var list = solution.Contents.ToArray();
+ _random.Shuffle(list);
+
+ int reagents = 0;
+ foreach (var (reagent, quantity) in list)
+ {
+ if (!_prototypeManager.TryIndex(reagent.Prototype, out var proto))
+ continue;
+
+ var mostToRemove = FixedPoint2.Zero;
+ if (proto.Metabolisms is null)
+ {
+ if (ent.Comp1.RemoveEmpty)
+ {
+ solution.RemoveReagent(reagent, FixedPoint2.New(1));
+ }
+
+ continue;
+ }
+
+ // we're done here entirely if this is true
+ if (reagents >= ent.Comp1.MaxReagentsProcessable)
+ return;
+
+
+ // loop over all our groups and see which ones apply
+ if (ent.Comp1.MetabolismGroups is null)
+ continue;
+
+ // TODO: Kill MetabolismGroups!
+ foreach (var group in ent.Comp1.MetabolismGroups)
+ {
+ if (!proto.Metabolisms.TryGetValue(group.Id, out var entry))
+ continue;
+
+ var rate = entry.MetabolismRate * group.MetabolismRateModifier;
+
+ // Remove $rate, as long as there's enough reagent there to actually remove that much
+ mostToRemove = FixedPoint2.Clamp(rate, 0, quantity);
+
+ var scale = (float) mostToRemove;
+
+ // TODO: This is a very stupid workaround to lungs heavily relying on scale = reagent quantity. Needs lung and metabolism refactors to remove.
+ // TODO: Lungs just need to have their scale be equal to the mols consumed, scale needs to be not hardcoded either and configurable per metabolizer...
+ if (group.Id != Gas)
+ scale /= (float) entry.MetabolismRate;
+
+ // if it's possible for them to be dead, and they are,
+ // then we shouldn't process any effects, but should probably
+ // still remove reagents
+ if (TryComp(solutionEntityUid.Value, out var state))
+ {
+ if (!proto.WorksOnTheDead && _mobStateSystem.IsDead(solutionEntityUid.Value, state))
+ continue;
+ }
+
+ var actualEntity = ent.Comp2?.Body ?? solutionEntityUid.Value;
+
+ // do all effects, if conditions apply
+ foreach (var effect in entry.Effects)
+ {
+ if (scale < effect.MinScale)
+ continue;
+
+ // See if conditions apply
+ if (effect.Conditions != null && !CanMetabolizeEffect(actualEntity, ent, soln.Value, effect.Conditions))
+ continue;
+
+ ApplyEffect(effect);
+
+ }
+
+ // TODO: We should have to do this with metabolism. ReagentEffect struct needs refactoring and so does metabolism!
+ void ApplyEffect(EntityEffect effect)
+ {
+ switch (effect)
+ {
+ case ModifyLungGas:
+ _entityEffects.ApplyEffect(ent, effect, scale);
+ break;
+ case AdjustReagent:
+ _entityEffects.ApplyEffect(soln.Value, effect, scale);
+ break;
+ default:
+ _entityEffects.ApplyEffect(actualEntity, effect, scale);
+ break;
+ }
+ }
+ }
+
+ // remove a certain amount of reagent
+ if (mostToRemove > FixedPoint2.Zero)
+ {
+ solution.RemoveReagent(reagent, mostToRemove);
+
+ // We have processed a reagant, so count it towards the cap
+ reagents += 1;
+ }
+ }
+
+ _solutionContainerSystem.UpdateChemicals(soln.Value);
+ }
+
+ ///
+ /// Public API to check if a certain metabolism effect can be applied to an entity.
+ /// TODO: With metabolism refactor make this logic smarter and unhardcode the old hardcoding entity effects used to have for metabolism!
+ ///
+ /// The body metabolizing the effects
+ /// The organ doing the metabolizing
+ /// The solution we are metabolizing from
+ /// The conditions that need to be met to metabolize
+ /// True if we can metabolize! False if we cannot!
+ public bool CanMetabolizeEffect(EntityUid body, EntityUid organ, Entity solution, EntityCondition[] conditions)
+ {
+ foreach (var condition in conditions)
+ {
+ switch (condition)
+ {
+ // Need specific handling of specific conditions since Metabolism is funny like that.
+ // TODO: MetabolizerTypes should be handled well before this stage by metabolism itself.
+ case MetabolizerTypeCondition:
+ if (_entityConditions.TryCondition(organ, condition))
+ continue;
+ break;
+ case ReagentCondition:
+ if (_entityConditions.TryCondition(solution, condition))
+ continue;
+ break;
+ default:
+ if (_entityConditions.TryCondition(body, condition))
+ continue;
+ break;
+ }
+
+ return false;
+ }
+
+ return true;
+ }
}
+
diff --git a/Content.Server/Body/Systems/RespiratorSystem.cs b/Content.Server/Body/Systems/RespiratorSystem.cs
index 63b04adc6a..2af7b24f26 100644
--- a/Content.Server/Body/Systems/RespiratorSystem.cs
+++ b/Content.Server/Body/Systems/RespiratorSystem.cs
@@ -2,7 +2,6 @@ using Content.Server.Administration.Logs;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Body.Components;
using Content.Server.Chat.Systems;
-using Content.Server.EntityEffects;
using Content.Shared.Body.Systems;
using Content.Shared.Alert;
using Content.Shared.Atmos;
@@ -14,9 +13,11 @@ using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Damage;
using Content.Shared.Database;
+using Content.Shared.EntityConditions;
+using Content.Shared.EntityConditions.Conditions.Body;
using Content.Shared.EntityEffects;
-using Content.Shared.EntityEffects.EffectConditions;
using Content.Shared.EntityEffects.Effects;
+using Content.Shared.EntityEffects.Effects.Body;
using Content.Shared.Mobs.Systems;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
@@ -29,16 +30,16 @@ public sealed class RespiratorSystem : EntitySystem
{
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
+ [Dependency] private readonly IPrototypeManager _protoMan = default!;
[Dependency] private readonly AlertsSystem _alertsSystem = default!;
[Dependency] private readonly AtmosphereSystem _atmosSys = default!;
[Dependency] private readonly BodySystem _bodySystem = default!;
+ [Dependency] private readonly ChatSystem _chat = default!;
[Dependency] private readonly DamageableSystem _damageableSys = default!;
[Dependency] private readonly LungSystem _lungSystem = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
- [Dependency] private readonly IPrototypeManager _protoMan = default!;
+ [Dependency] private readonly SharedEntityConditionsSystem _entityConditions = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
- [Dependency] private readonly ChatSystem _chat = default!;
- [Dependency] private readonly EntityEffectSystem _entityEffect = default!;
private static readonly ProtoId GasId = new("Gas");
@@ -340,7 +341,6 @@ public sealed class RespiratorSystem : EntitySystem
}
}
- // TODO generalize condition checks
// this is pretty janky, but I just want to bodge a method that checks if an entity can breathe a gas mixture
// Applying actual reaction effects require a full ReagentEffectArgs struct.
bool CanMetabolize(EntityEffect effect)
@@ -348,9 +348,10 @@ public sealed class RespiratorSystem : EntitySystem
if (effect.Conditions == null)
return true;
+ // TODO: Use Metabolism Public API to do this instead, once that API has been built.
foreach (var cond in effect.Conditions)
{
- if (cond is OrganType organ && !_entityEffect.OrganCondition(organ, lung))
+ if (cond is MetabolizerTypeCondition organ && !_entityConditions.TryCondition(lung, organ))
return false;
}
diff --git a/Content.Server/Body/Systems/ThermalRegulatorSystem.cs b/Content.Server/Body/Systems/ThermalRegulatorSystem.cs
index 3ba9e6af31..af7b17643e 100644
--- a/Content.Server/Body/Systems/ThermalRegulatorSystem.cs
+++ b/Content.Server/Body/Systems/ThermalRegulatorSystem.cs
@@ -1,7 +1,7 @@
using Content.Server.Body.Components;
-using Content.Server.Temperature.Components;
using Content.Server.Temperature.Systems;
using Content.Shared.ActionBlocker;
+using Content.Shared.Temperature.Components;
using Robust.Shared.Timing;
namespace Content.Server.Body.Systems;
diff --git a/Content.Server/Botany/SeedPrototype.cs b/Content.Server/Botany/SeedPrototype.cs
index ee7ca4f584..253eea2df9 100644
--- a/Content.Server/Botany/SeedPrototype.cs
+++ b/Content.Server/Botany/SeedPrototype.cs
@@ -1,8 +1,9 @@
using Content.Server.Botany.Components;
using Content.Server.Botany.Systems;
-using Content.Server.EntityEffects;
+using Content.Server.EntityEffects.Effects.Botany;
using Content.Shared.Atmos;
using Content.Shared.Database;
+using Content.Shared.EntityEffects;
using Content.Shared.Random;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
@@ -79,9 +80,13 @@ public partial struct SeedChemQuantity
[DataField("Inherent")] public bool Inherent = true;
}
-// TODO reduce the number of friends to a reasonable level. Requires ECS-ing things like plant holder component.
+// TODO Make Botany ECS and give it a proper API. I removed the limited access of this class because it's egregious how many systems needed access to it due to a lack of an actual API.
+///
+/// SeedData is no longer restricted because the number of friends is absolutely unreasonable.
+/// This entire data definition is unreasonable. I felt genuine fear looking at this, this is horrific. Send help.
+///
+// TODO: Hit Botany with hammers
[Virtual, DataDefinition]
-[Access(typeof(BotanySystem), typeof(PlantHolderSystem), typeof(SeedExtractorSystem), typeof(PlantHolderComponent), typeof(EntityEffectSystem), typeof(MutationSystem))]
public partial class SeedData
{
#region Tracking
diff --git a/Content.Server/Botany/Systems/BotanySystem.Produce.cs b/Content.Server/Botany/Systems/BotanySystem.Produce.cs
index f6f3f99c09..7d8f8652c7 100644
--- a/Content.Server/Botany/Systems/BotanySystem.Produce.cs
+++ b/Content.Server/Botany/Systems/BotanySystem.Produce.cs
@@ -7,6 +7,8 @@ namespace Content.Server.Botany.Systems;
public sealed partial class BotanySystem
{
+ [Dependency] private readonly SharedEntityEffectsSystem _entityEffects = default!;
+
public void ProduceGrown(EntityUid uid, ProduceComponent produce)
{
if (!TryGetSeed(produce, out var seed))
@@ -15,10 +17,7 @@ public sealed partial class BotanySystem
foreach (var mutation in seed.Mutations)
{
if (mutation.AppliesToProduce)
- {
- var args = new EntityEffectBaseArgs(uid, EntityManager);
- mutation.Effect.Effect(args);
- }
+ _entityEffects.TryApplyEffect(uid, mutation.Effect);
}
if (!_solutionContainerSystem.EnsureSolution(uid,
diff --git a/Content.Server/Botany/Systems/MutationSystem.cs b/Content.Server/Botany/Systems/MutationSystem.cs
index ee35db48e3..834fd9e8ef 100644
--- a/Content.Server/Botany/Systems/MutationSystem.cs
+++ b/Content.Server/Botany/Systems/MutationSystem.cs
@@ -13,6 +13,7 @@ public sealed class MutationSystem : EntitySystem
[Dependency] private readonly IRobustRandom _robustRandom = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly SharedEntityEffectsSystem _entityEffects = default!;
private RandomPlantMutationListPrototype _randomMutations = default!;
public override void Initialize()
@@ -32,10 +33,8 @@ public sealed class MutationSystem : EntitySystem
if (Random(Math.Min(mutation.BaseOdds * severity, 1.0f)))
{
if (mutation.AppliesToPlant)
- {
- var args = new EntityEffectBaseArgs(plantHolder, EntityManager);
- mutation.Effect.Effect(args);
- }
+ _entityEffects.TryApplyEffect(plantHolder, mutation.Effect);
+
// Stat adjustments do not persist by being an attached effect, they just change the stat.
if (mutation.Persists && !seed.Mutations.Any(m => m.Name == mutation.Name))
seed.Mutations.Add(mutation);
diff --git a/Content.Server/Botany/Systems/PlantHolderSystem.cs b/Content.Server/Botany/Systems/PlantHolderSystem.cs
index caa796efe2..2554f95455 100644
--- a/Content.Server/Botany/Systems/PlantHolderSystem.cs
+++ b/Content.Server/Botany/Systems/PlantHolderSystem.cs
@@ -24,8 +24,10 @@ using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using Content.Shared.Administration.Logs;
+using Content.Shared.Chemistry.Reaction;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Database;
+using Content.Shared.EntityEffects;
using Content.Shared.Kitchen.Components;
using Content.Shared.Labels.Components;
@@ -48,6 +50,7 @@ public sealed class PlantHolderSystem : EntitySystem
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly ItemSlotsSystem _itemSlots = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
+ [Dependency] private readonly SharedEntityEffectsSystem _entityEffects = default!;
public const float HydroponicsSpeedMultiplier = 1f;
public const float HydroponicsConsumptionMultiplier = 2f;
@@ -887,7 +890,7 @@ public sealed class PlantHolderSystem : EntitySystem
foreach (var entry in _solutionContainerSystem.RemoveEachReagent(component.SoilSolution.Value, amt))
{
var reagentProto = _prototype.Index(entry.Reagent.Prototype);
- reagentProto.ReactionPlant(uid, entry, solution, EntityManager, _random, _adminLogger);
+ _entityEffects.ApplyEffects(uid, reagentProto.PlantMetabolisms.ToArray());
}
}
diff --git a/Content.Server/Chemistry/Commands/DumpReagentGuideText.cs b/Content.Server/Chemistry/Commands/DumpReagentGuideText.cs
index a70c2196ab..58c86f058a 100644
--- a/Content.Server/Chemistry/Commands/DumpReagentGuideText.cs
+++ b/Content.Server/Chemistry/Commands/DumpReagentGuideText.cs
@@ -39,7 +39,7 @@ public sealed class DumpReagentGuideText : LocalizedEntityCommands
{
foreach (var effect in entry.Effects)
{
- shell.WriteLine(effect.GuidebookEffectDescription(_prototype, EntityManager.EntitySysManager) ??
+ shell.WriteLine(reagent.GuidebookReagentEffectDescription(_prototype, EntityManager.EntitySysManager, effect, entry.MetabolismRate) ??
Loc.GetString($"cmd-dumpreagentguidetext-skipped", ("effect", effect.GetType())));
}
}
diff --git a/Content.Server/Construction/ConstructionSystem.Interactions.cs b/Content.Server/Construction/ConstructionSystem.Interactions.cs
index 3dd5a5b794..77a1a63e02 100644
--- a/Content.Server/Construction/ConstructionSystem.Interactions.cs
+++ b/Content.Server/Construction/ConstructionSystem.Interactions.cs
@@ -13,6 +13,7 @@ using Content.Shared.Prying.Systems;
using Content.Shared.Radio.EntitySystems;
using Content.Shared.Stacks;
using Content.Shared.Temperature;
+using Content.Shared.Temperature.Components;
using Content.Shared.Tools.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Utility;
diff --git a/Content.Server/EntityConditions/Conditions/BreathingEntityConditionSystem.cs b/Content.Server/EntityConditions/Conditions/BreathingEntityConditionSystem.cs
new file mode 100644
index 0000000000..e7b8aaf22b
--- /dev/null
+++ b/Content.Server/EntityConditions/Conditions/BreathingEntityConditionSystem.cs
@@ -0,0 +1,19 @@
+using Content.Server.Body.Components;
+using Content.Server.Body.Systems;
+using Content.Shared.EntityConditions;
+using Content.Shared.EntityConditions.Conditions.Body;
+
+namespace Content.Server.EntityConditions.Conditions;
+
+///
+/// Returns true if this entity is both able to breathe and is currently breathing.
+///
+///
+public sealed partial class IsBreathingEntityConditionSystem : EntityConditionSystem
+{
+ [Dependency] private readonly RespiratorSystem _respirator = default!;
+ protected override void Condition(Entity entity, ref EntityConditionEvent args)
+ {
+ args.Result = _respirator.IsBreathing(entity.AsNullable());
+ }
+}
diff --git a/Content.Server/EntityConditions/Conditions/MetabolizerTypesEntityConditionSystem.cs b/Content.Server/EntityConditions/Conditions/MetabolizerTypesEntityConditionSystem.cs
new file mode 100644
index 0000000000..3b4fb5292b
--- /dev/null
+++ b/Content.Server/EntityConditions/Conditions/MetabolizerTypesEntityConditionSystem.cs
@@ -0,0 +1,21 @@
+using System.Linq;
+using Content.Server.Body.Components;
+using Content.Shared.EntityConditions;
+using Content.Shared.EntityConditions.Conditions.Body;
+
+namespace Content.Server.EntityConditions.Conditions;
+
+///
+/// Returns true if this entity has any of the listed metabolizer types.
+///
+///
+public sealed partial class MetabolizerTypeEntityConditionSystem : EntityConditionSystem
+{
+ protected override void Condition(Entity entity, ref EntityConditionEvent args)
+ {
+ if (entity.Comp.MetabolizerTypes == null)
+ return;
+
+ args.Result = entity.Comp.MetabolizerTypes.Overlaps(args.Condition.Type);
+ }
+}
diff --git a/Content.Server/EntityEffects/Effects/Atmos/CreateGasEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Atmos/CreateGasEntityEffectSystem.cs
new file mode 100644
index 0000000000..033704ffcd
--- /dev/null
+++ b/Content.Server/EntityEffects/Effects/Atmos/CreateGasEntityEffectSystem.cs
@@ -0,0 +1,22 @@
+using Content.Server.Atmos.EntitySystems;
+using Content.Shared.EntityEffects;
+using Content.Shared.EntityEffects.Effects.Atmos;
+
+namespace Content.Server.EntityEffects.Effects.Atmos;
+
+///
+/// This effect adjusts a gas at the tile this entity is currently on.
+/// The amount changed is modified by scale.
+///
+///
+public sealed partial class CreateGasEntityEffectSystem : EntityEffectSystem
+{
+ [Dependency] private readonly AtmosphereSystem _atmosphere = default!;
+
+ protected override void Effect(Entity entity, ref EntityEffectEvent args)
+ {
+ var tileMix = _atmosphere.GetContainingMixture(entity.AsNullable(), false, true);
+
+ tileMix?.AdjustMoles(args.Effect.Gas, args.Scale * args.Effect.Moles);
+ }
+}
diff --git a/Content.Server/EntityEffects/Effects/Atmos/FlammableEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Atmos/FlammableEntityEffectSystem.cs
new file mode 100644
index 0000000000..65c818f143
--- /dev/null
+++ b/Content.Server/EntityEffects/Effects/Atmos/FlammableEntityEffectSystem.cs
@@ -0,0 +1,25 @@
+using Content.Server.Atmos.EntitySystems;
+using Content.Shared.Atmos.Components;
+using Content.Shared.EntityEffects;
+using Content.Shared.EntityEffects.Effects.Atmos;
+
+namespace Content.Server.EntityEffects.Effects.Atmos;
+
+///
+/// Adds a number of FireStacks modified by scale to this entity.
+/// The amount of FireStacks added is modified by scale.
+///
+///
+public sealed partial class FlammableEntityEffectSystem : EntityEffectSystem
+{
+ [Dependency] private readonly FlammableSystem _flammable = default!;
+
+ protected override void Effect(Entity entity, ref EntityEffectEvent args)
+ {
+ // The multiplier is determined by if the entity is already on fire, and if the multiplier for existing FireStacks has a value.
+ // If both of these are true, we use the MultiplierOnExisting value, otherwise we use the standard Multiplier.
+ var multiplier = entity.Comp.FireStacks == 0f || args.Effect.MultiplierOnExisting == null ? args.Effect.Multiplier : args.Effect.MultiplierOnExisting.Value;
+
+ _flammable.AdjustFireStacks(entity, args.Scale * multiplier, entity.Comp);
+ }
+}
diff --git a/Content.Server/EntityEffects/Effects/Atmos/IgniteEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Atmos/IgniteEntityEffectSystem.cs
new file mode 100644
index 0000000000..de90656c66
--- /dev/null
+++ b/Content.Server/EntityEffects/Effects/Atmos/IgniteEntityEffectSystem.cs
@@ -0,0 +1,23 @@
+using Content.Server.Atmos.EntitySystems;
+using Content.Shared.Atmos.Components;
+using Content.Shared.EntityEffects;
+using Content.Shared.EntityEffects.Effects.Atmos;
+
+namespace Content.Server.EntityEffects.Effects.Atmos;
+
+///
+/// Sets this entity on fire.
+///
+///
+public sealed partial class IngiteEntityEffectSystem : EntityEffectSystem
+{
+ [Dependency] private readonly FlammableSystem _flammable = default!;
+
+ protected override void Effect(Entity entity, ref EntityEffectEvent args)
+ {
+ // TODO: Proper BodySystem Metabolism Effect relay...
+ // TODO: If this fucks over downstream shitmed, I give you full approval to use whatever shitcode method you need to fix it. Metabolism is awful.
+ _flammable.Ignite(entity, entity, flammable: entity.Comp);
+ }
+}
+
diff --git a/Content.Server/EntityEffects/Effects/Body/OxygenateEntityEffectsSystem.cs b/Content.Server/EntityEffects/Effects/Body/OxygenateEntityEffectsSystem.cs
new file mode 100644
index 0000000000..0cbf0b3864
--- /dev/null
+++ b/Content.Server/EntityEffects/Effects/Body/OxygenateEntityEffectsSystem.cs
@@ -0,0 +1,20 @@
+using Content.Server.Body.Components;
+using Content.Server.Body.Systems;
+using Content.Shared.EntityEffects;
+using Content.Shared.EntityEffects.Effects.Body;
+
+namespace Content.Server.EntityEffects.Effects.Body;
+
+///
+/// This effect adjusts a respirator's saturation value.
+/// The saturation adjustment is modified by scale.
+///
+///
+public sealed partial class OxygenateEntityEffectsSystem : EntityEffectSystem
+{
+ [Dependency] private readonly RespiratorSystem _respirator = default!;
+ protected override void Effect(Entity entity, ref EntityEffectEvent args)
+ {
+ _respirator.UpdateSaturation(entity, args.Scale * args.Effect.Factor, entity.Comp);
+ }
+}
diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustHealthEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustHealthEntityEffectSystem.cs
new file mode 100644
index 0000000000..64f61f5b11
--- /dev/null
+++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustHealthEntityEffectSystem.cs
@@ -0,0 +1,20 @@
+using Content.Server.Botany.Components;
+using Content.Server.Botany.Systems;
+using Content.Shared.EntityEffects;
+using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes;
+
+namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes;
+
+public sealed partial class PlantAdjustHealthEntityEffectSystem : EntityEffectSystem
+{
+ [Dependency] private readonly PlantHolderSystem _plantHolder = default!;
+
+ protected override void Effect(Entity entity, ref EntityEffectEvent args)
+ {
+ if (entity.Comp.Seed == null || entity.Comp.Dead)
+ return;
+
+ entity.Comp.MutationLevel += args.Effect.Amount * entity.Comp.MutationMod;
+ _plantHolder.CheckHealth(entity, entity.Comp);
+ }
+}
diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustMutationLevelEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustMutationLevelEntityEffectSystem.cs
new file mode 100644
index 0000000000..f35ff25b25
--- /dev/null
+++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustMutationLevelEntityEffectSystem.cs
@@ -0,0 +1,20 @@
+using Content.Server.Botany.Components;
+using Content.Server.Botany.Systems;
+using Content.Shared.EntityEffects;
+using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes;
+
+namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes;
+
+public sealed partial class PlantAdjustMutationLevelEntityEffectSystem : EntityEffectSystem
+{
+ [Dependency] private readonly PlantHolderSystem _plantHolder = default!;
+
+ protected override void Effect(Entity entity, ref EntityEffectEvent args)
+ {
+ if (entity.Comp.Seed == null || entity.Comp.Dead)
+ return;
+
+ entity.Comp.Health += args.Effect.Amount;
+ _plantHolder.CheckHealth(entity, entity.Comp);
+ }
+}
diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustMutationModEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustMutationModEntityEffectSystem.cs
new file mode 100644
index 0000000000..3163ee374c
--- /dev/null
+++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustMutationModEntityEffectSystem.cs
@@ -0,0 +1,16 @@
+using Content.Server.Botany.Components;
+using Content.Shared.EntityEffects;
+using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes;
+
+namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes;
+
+public sealed partial class PlantAdjustMutationModEntityEffectSystem : EntityEffectSystem
+{
+ protected override void Effect(Entity entity, ref EntityEffectEvent args)
+ {
+ if (entity.Comp.Seed == null || entity.Comp.Dead)
+ return;
+
+ entity.Comp.MutationMod += args.Effect.Amount;
+ }
+}
diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustNutritionEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustNutritionEntityEffectSystem.cs
new file mode 100644
index 0000000000..56c016700d
--- /dev/null
+++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustNutritionEntityEffectSystem.cs
@@ -0,0 +1,16 @@
+using Content.Server.Botany.Components;
+using Content.Server.Botany.Systems;
+using Content.Shared.EntityEffects;
+using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes;
+
+namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes;
+
+public sealed partial class PlantAdjustNutritionEntityEffectSystem : EntityEffectSystem
+{
+ [Dependency] private readonly PlantHolderSystem _plantHolder = default!;
+
+ protected override void Effect(Entity entity, ref EntityEffectEvent args)
+ {
+ _plantHolder.AdjustNutrient(entity, args.Effect.Amount, entity);
+ }
+}
diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustPestsEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustPestsEntityEffectSystem.cs
new file mode 100644
index 0000000000..0495034b38
--- /dev/null
+++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustPestsEntityEffectSystem.cs
@@ -0,0 +1,16 @@
+using Content.Server.Botany.Components;
+using Content.Shared.EntityEffects;
+using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes;
+
+namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes;
+
+public sealed partial class PlantAdjustPestsEntityEffectSystem : EntityEffectSystem
+{
+ protected override void Effect(Entity entity, ref EntityEffectEvent args)
+ {
+ if (entity.Comp.Seed == null || entity.Comp.Dead)
+ return;
+
+ entity.Comp.PestLevel += args.Effect.Amount;
+ }
+}
diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustPotencyEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustPotencyEntityEffectSystem.cs
new file mode 100644
index 0000000000..ebe5c83181
--- /dev/null
+++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustPotencyEntityEffectSystem.cs
@@ -0,0 +1,19 @@
+using Content.Server.Botany.Components;
+using Content.Server.Botany.Systems;
+using Content.Shared.EntityEffects;
+using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes;
+
+namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes;
+
+public sealed partial class PlantAdjustPotencyEntityEffectSystem : EntityEffectSystem
+{
+ [Dependency] private readonly PlantHolderSystem _plantHolder = default!;
+ protected override void Effect(Entity entity, ref EntityEffectEvent args)
+ {
+ if (entity.Comp.Seed == null || entity.Comp.Dead)
+ return;
+
+ _plantHolder.EnsureUniqueSeed(entity, entity.Comp);
+ entity.Comp.Seed.Potency = Math.Max(entity.Comp.Seed.Potency + args.Effect.Amount, 1);
+ }
+}
diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustToxinsEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustToxinsEntityEffectSystem.cs
new file mode 100644
index 0000000000..31dc328977
--- /dev/null
+++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustToxinsEntityEffectSystem.cs
@@ -0,0 +1,16 @@
+using Content.Server.Botany.Components;
+using Content.Shared.EntityEffects;
+using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes;
+
+namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes;
+
+public sealed partial class PlantAdjustToxinsEntityEffectSystem : EntityEffectSystem
+{
+ protected override void Effect(Entity entity, ref EntityEffectEvent args)
+ {
+ if (entity.Comp.Seed == null || entity.Comp.Dead)
+ return;
+
+ entity.Comp.Toxins += args.Effect.Amount;
+ }
+}
diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustWaterEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustWaterEntityEffectSystem.cs
new file mode 100644
index 0000000000..706eeb2ffe
--- /dev/null
+++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustWaterEntityEffectSystem.cs
@@ -0,0 +1,16 @@
+using Content.Server.Botany.Components;
+using Content.Server.Botany.Systems;
+using Content.Shared.EntityEffects;
+using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes;
+
+namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes;
+
+public sealed partial class PlantAdjustWaterEntityEffectSystem : EntityEffectSystem
+{
+ [Dependency] private readonly PlantHolderSystem _plantHolder = default!;
+
+ protected override void Effect(Entity entity, ref EntityEffectEvent args)
+ {
+ _plantHolder.AdjustWater(entity, args.Effect.Amount, entity.Comp);
+ }
+}
diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustWeedsEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustWeedsEntityEffectSystem.cs
new file mode 100644
index 0000000000..34aa51e4ff
--- /dev/null
+++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAdjustWeedsEntityEffectSystem.cs
@@ -0,0 +1,16 @@
+using Content.Server.Botany.Components;
+using Content.Shared.EntityEffects;
+using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes;
+
+namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes;
+
+public sealed partial class PlantAdjustWeedsEntityEffectSystem : EntityEffectSystem
+{
+ protected override void Effect(Entity entity, ref EntityEffectEvent args)
+ {
+ if (entity.Comp.Seed == null || entity.Comp.Dead)
+ return;
+
+ entity.Comp.WeedLevel += args.Effect.Amount;
+ }
+}
diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAffectGrowthEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAffectGrowthEntityEffectSystem.cs
new file mode 100644
index 0000000000..b0faa6255e
--- /dev/null
+++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantAffectGrowthEntityEffectSystem.cs
@@ -0,0 +1,19 @@
+using Content.Server.Botany.Components;
+using Content.Server.Botany.Systems;
+using Content.Shared.EntityEffects;
+using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes;
+
+namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes;
+
+public sealed partial class PlantAffectGrowthEntityEffectSystem : EntityEffectSystem
+{
+ [Dependency] private readonly PlantHolderSystem _plantHolder = default!;
+
+ protected override void Effect(Entity entity, ref EntityEffectEvent args)
+ {
+ if (entity.Comp.Seed == null || entity.Comp.Dead)
+ return;
+
+ _plantHolder.AffectGrowth(entity, (int)args.Effect.Amount, entity);
+ }
+}
diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantChangeStatEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantChangeStatEntityEffectSystem.cs
new file mode 100644
index 0000000000..3d82f74b11
--- /dev/null
+++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantChangeStatEntityEffectSystem.cs
@@ -0,0 +1,122 @@
+using Content.Server.Botany.Components;
+using Content.Shared.EntityEffects;
+using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes;
+using Robust.Shared.Random;
+
+namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes;
+
+///
+/// This system mutates an inputted stat for a PlantHolder, only works for floats, integers, and bools.
+///
+///
+public sealed partial class PlantChangeStatEntityEffectSystem : EntityEffectSystem
+{
+ // TODO: This is awful. I do not have the strength to refactor this. I want it gone.
+ [Dependency] private readonly IRobustRandom _random = default!;
+
+ protected override void Effect(Entity entity, ref EntityEffectEvent args)
+ {
+ if (entity.Comp.Seed == null || entity.Comp.Dead)
+ return;
+
+ var effect = args.Effect;
+ var member = entity.Comp.Seed.GetType().GetField(args.Effect.TargetValue);
+
+ if (member == null)
+ {
+ Log.Error($"{ effect.GetType().Name } Error: Member { args.Effect.TargetValue} not found on { entity.Comp.Seed.GetType().Name }. Did you misspell it?");
+ return;
+ }
+
+ var currentValObj = member.GetValue(entity.Comp.Seed);
+ if (currentValObj == null)
+ return;
+
+ if (member.FieldType == typeof(float))
+ {
+ var floatVal = (float)currentValObj;
+ MutateFloat(ref floatVal, args.Effect.MinValue, args.Effect.MaxValue, args.Effect.Steps);
+ member.SetValue(entity.Comp.Seed, floatVal);
+ }
+ else if (member.FieldType == typeof(int))
+ {
+ var intVal = (int)currentValObj;
+ MutateInt(ref intVal, (int)args.Effect.MinValue, (int)args.Effect.MaxValue, args.Effect.Steps);
+ member.SetValue(entity.Comp.Seed, intVal);
+ }
+ else if (member.FieldType == typeof(bool))
+ {
+ var boolVal = (bool)currentValObj;
+ boolVal = !boolVal;
+ member.SetValue(entity.Comp.Seed, boolVal);
+ }
+ }
+
+ // Mutate reference 'val' between 'min' and 'max' by pretending the value
+ // is representable by a thermometer code with 'bits' number of bits and
+ // randomly flipping some of them.
+ private void MutateFloat(ref float val, float min, float max, int bits)
+ {
+ if (min == max)
+ {
+ val = min;
+ return;
+ }
+
+ // Starting number of bits that are high, between 0 and bits.
+ // In other words, it's val mapped linearly from range [min, max] to range [0, bits], and then rounded.
+ int valInt = (int)MathF.Round((val - min) / (max - min) * bits);
+ // val may be outside the range of min/max due to starting prototype values, so clamp.
+ valInt = Math.Clamp(valInt, 0, bits);
+
+ // Probability that the bit flip increases n.
+ // The higher the current value is, the lower the probability of increasing value is, and the higher the probability of decreasive it it.
+ // In other words, it tends to go to the middle.
+ float probIncrease = 1 - (float)valInt / bits;
+ int valIntMutated;
+ if (_random.Prob(probIncrease))
+ {
+ valIntMutated = valInt + 1;
+ }
+ else
+ {
+ valIntMutated = valInt - 1;
+ }
+
+ // Set value based on mutated thermometer code.
+ float valMutated = Math.Clamp((float)valIntMutated / bits * (max - min) + min, min, max);
+ val = valMutated;
+ }
+
+ private void MutateInt(ref int val, int min, int max, int bits)
+ {
+ if (min == max)
+ {
+ val = min;
+ return;
+ }
+
+ // Starting number of bits that are high, between 0 and bits.
+ // In other words, it's val mapped linearly from range [min, max] to range [0, bits], and then rounded.
+ int valInt = (int)MathF.Round((val - min) / (max - min) * bits);
+ // val may be outside the range of min/max due to starting prototype values, so clamp.
+ valInt = Math.Clamp(valInt, 0, bits);
+
+ // Probability that the bit flip increases n.
+ // The higher the current value is, the lower the probability of increasing value is, and the higher the probability of decreasing it.
+ // In other words, it tends to go to the middle.
+ float probIncrease = 1 - (float)valInt / bits;
+ int valMutated;
+ if (_random.Prob(probIncrease))
+ {
+ valMutated = val + 1;
+ }
+ else
+ {
+ valMutated = val - 1;
+ }
+
+ valMutated = Math.Clamp(valMutated, min, max);
+ val = valMutated;
+ }
+}
diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantCryoxadoneEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantCryoxadoneEntityEffectSystem.cs
new file mode 100644
index 0000000000..710bce24dd
--- /dev/null
+++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantCryoxadoneEntityEffectSystem.cs
@@ -0,0 +1,30 @@
+using Content.Server.Botany.Components;
+using Content.Shared.EntityEffects;
+using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes;
+using Robust.Shared.Random;
+
+namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes;
+
+public sealed partial class PlantCryoxadoneEntityEffectSystem : EntityEffectSystem
+{
+ [Dependency] private readonly IRobustRandom _random = default!;
+
+ protected override void Effect(Entity entity, ref EntityEffectEvent args)
+ {
+ if (entity.Comp.Seed == null || entity.Comp.Dead)
+ return;
+
+ var deviation = 0;
+ var seed = entity.Comp.Seed;
+ if (seed == null)
+ return;
+ if (entity.Comp.Age > seed.Maturation)
+ deviation = (int) Math.Max(seed.Maturation - 1, entity.Comp.Age - _random.Next(7, 10));
+ else
+ deviation = (int) (seed.Maturation / seed.GrowthStages);
+ entity.Comp.Age -= deviation;
+ entity.Comp.LastProduce = entity.Comp.Age;
+ entity.Comp.SkipAging++;
+ entity.Comp.ForceUpdate = true;
+ }
+}
diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantDestroySeedsEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantDestroySeedsEntityEffectSystem.cs
new file mode 100644
index 0000000000..1661c501be
--- /dev/null
+++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantDestroySeedsEntityEffectSystem.cs
@@ -0,0 +1,31 @@
+using Content.Server.Botany.Components;
+using Content.Server.Botany.Systems;
+using Content.Server.Popups;
+using Content.Shared.EntityEffects;
+using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes;
+using Content.Shared.Popups;
+
+namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes;
+
+public sealed partial class PlantDestroySeedsEntityEffectSystem : EntityEffectSystem
+{
+ [Dependency] private readonly PlantHolderSystem _plantHolder = default!;
+ [Dependency] private readonly PopupSystem _popup = default!;
+
+ protected override void Effect(Entity entity, ref EntityEffectEvent args)
+ {
+ if (entity.Comp.Seed == null || entity.Comp.Dead || entity.Comp.Seed.Immutable)
+ return;
+
+ if (entity.Comp.Seed.Seedless)
+ return;
+
+ _plantHolder.EnsureUniqueSeed(entity, entity.Comp);
+ _popup.PopupEntity(
+ Loc.GetString("botany-plant-seedsdestroyed"),
+ entity,
+ PopupType.SmallCaution
+ );
+ entity.Comp.Seed.Seedless = true;
+ }
+}
diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantDiethylamineEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantDiethylamineEntityEffectSystem.cs
new file mode 100644
index 0000000000..f6aebde465
--- /dev/null
+++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantDiethylamineEntityEffectSystem.cs
@@ -0,0 +1,31 @@
+using Content.Server.Botany.Components;
+using Content.Server.Botany.Systems;
+using Content.Shared.EntityEffects;
+using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes;
+using Robust.Shared.Random;
+
+namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes;
+
+public sealed partial class PlantDiethylamineEntityEffectSystem : EntityEffectSystem
+{
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly PlantHolderSystem _plantHolder = default!;
+
+ protected override void Effect(Entity entity, ref EntityEffectEvent args)
+ {
+ if (entity.Comp.Seed == null || entity.Comp.Dead || entity.Comp.Seed.Immutable)
+ return;
+
+ if (_random.Prob(0.1f))
+ {
+ _plantHolder.EnsureUniqueSeed(entity, entity);
+ entity.Comp.Seed!.Lifespan++;
+ }
+
+ if (_random.Prob(0.1f))
+ {
+ _plantHolder.EnsureUniqueSeed(entity, entity);
+ entity.Comp.Seed!.Endurance++;
+ }
+ }
+}
diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantPhalanximineEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantPhalanximineEntityEffectSystem.cs
new file mode 100644
index 0000000000..8a073392e1
--- /dev/null
+++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantPhalanximineEntityEffectSystem.cs
@@ -0,0 +1,16 @@
+using Content.Server.Botany.Components;
+using Content.Shared.EntityEffects;
+using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes;
+
+namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes;
+
+public sealed partial class PlantPhalanximineEntityEffectSystem : EntityEffectSystem
+{
+ protected override void Effect(Entity entity, ref EntityEffectEvent args)
+ {
+ if (entity.Comp.Seed == null || entity.Comp.Dead || entity.Comp.Seed.Immutable)
+ return;
+
+ entity.Comp.Seed.Viable = true;
+ }
+}
diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantRestoreSeedsEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantRestoreSeedsEntityEffectSystem.cs
new file mode 100644
index 0000000000..4d724be244
--- /dev/null
+++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/PlantRestoreSeedsEntityEffectSystem.cs
@@ -0,0 +1,26 @@
+using Content.Server.Botany.Components;
+using Content.Server.Botany.Systems;
+using Content.Server.Popups;
+using Content.Shared.EntityEffects;
+using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes;
+
+namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes;
+
+public sealed partial class PlantRestoreSeedsEntityEffectSystem : EntityEffectSystem
+{
+ [Dependency] private readonly PlantHolderSystem _plantHolder = default!;
+ [Dependency] private readonly PopupSystem _popup = default!;
+
+ protected override void Effect(Entity entity, ref EntityEffectEvent args)
+ {
+ if (entity.Comp.Seed == null || entity.Comp.Dead || entity.Comp.Seed.Immutable)
+ return;
+
+ if (!entity.Comp.Seed.Seedless)
+ return;
+
+ _plantHolder.EnsureUniqueSeed(entity, entity.Comp);
+ _popup.PopupEntity(Loc.GetString("botany-plant-seedsrestored"), entity);
+ entity.Comp.Seed.Seedless = false;
+ }
+}
diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/RobustHarvestEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/RobustHarvestEntityEffectSystem.cs
new file mode 100644
index 0000000000..68ea3319ef
--- /dev/null
+++ b/Content.Server/EntityEffects/Effects/Botany/PlantAttributes/RobustHarvestEntityEffectSystem.cs
@@ -0,0 +1,41 @@
+using Content.Server.Botany.Components;
+using Content.Server.Botany.Systems;
+using Content.Shared.EntityEffects;
+using Content.Shared.EntityEffects.Effects.Botany.PlantAttributes;
+using Robust.Shared.Random;
+
+namespace Content.Server.EntityEffects.Effects.Botany.PlantAttributes;
+
+///
+/// This effect directly increases the potency of a PlantHolder's plant provided it exists and isn't dead.
+/// Potency directly correlates to the size of the plant's produce.
+///
+///
+public sealed partial class RobustHarvestEntityEffectSystem : EntityEffectSystem
+{
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly PlantHolderSystem _plantHolder = default!;
+
+ protected override void Effect(Entity entity, ref EntityEffectEvent args)
+ {
+ if (entity.Comp.Seed == null || entity.Comp.Dead)
+ return;
+
+ if (entity.Comp.Seed.Potency < args.Effect.PotencyLimit)
+ {
+ _plantHolder.EnsureUniqueSeed(entity, entity.Comp);
+ entity.Comp.Seed.Potency = Math.Min(entity.Comp.Seed.Potency + args.Effect.PotencyIncrease, args.Effect.PotencyLimit);
+
+ if (entity.Comp.Seed.Potency > args.Effect.PotencySeedlessThreshold)
+ {
+ entity.Comp.Seed.Seedless = true;
+ }
+ }
+ else if (entity.Comp.Seed.Yield > 1 && _random.Prob(0.1f))
+ {
+ // Too much of a good thing reduces yield
+ _plantHolder.EnsureUniqueSeed(entity, entity.Comp);
+ entity.Comp.Seed.Yield--;
+ }
+ }
+}
diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantMutateChemicalsEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantMutateChemicalsEntityEffectSystem.cs
new file mode 100644
index 0000000000..120ae6e881
--- /dev/null
+++ b/Content.Server/EntityEffects/Effects/Botany/PlantMutateChemicalsEntityEffectSystem.cs
@@ -0,0 +1,43 @@
+using Content.Server.Botany;
+using Content.Server.Botany.Components;
+using Content.Shared.EntityEffects;
+using Content.Shared.EntityEffects.Effects.Botany;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+
+namespace Content.Server.EntityEffects.Effects.Botany;
+
+public sealed partial class PlantMutateChemicalsEntityEffectSystem : EntityEffectSystem
+{
+ [Dependency] private readonly IPrototypeManager _proto = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+
+ protected override void Effect(Entity entity, ref EntityEffectEvent args)
+ {
+ if (entity.Comp.Seed == null)
+ return;
+
+ var chemicals = entity.Comp.Seed.Chemicals;
+ var randomChems = _proto.Index(args.Effect.RandomPickBotanyReagent).Fills;
+
+ // Add a random amount of a random chemical to this set of chemicals
+ var pick = _random.Pick(randomChems);
+ var chemicalId = _random.Pick(pick.Reagents);
+ var amount = _random.Next(1, (int)pick.Quantity);
+ var seedChemQuantity = new SeedChemQuantity();
+ if (chemicals.ContainsKey(chemicalId))
+ {
+ seedChemQuantity.Min = chemicals[chemicalId].Min;
+ seedChemQuantity.Max = chemicals[chemicalId].Max + amount;
+ }
+ else
+ {
+ seedChemQuantity.Min = 1;
+ seedChemQuantity.Max = 1 + amount;
+ seedChemQuantity.Inherent = false;
+ }
+ var potencyDivisor = (int)Math.Ceiling(100.0f / seedChemQuantity.Max);
+ seedChemQuantity.PotencyDivisor = potencyDivisor;
+ chemicals[chemicalId] = seedChemQuantity;
+ }
+}
diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantMutateGasesEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantMutateGasesEntityEffectSystem.cs
new file mode 100644
index 0000000000..e2376ba186
--- /dev/null
+++ b/Content.Server/EntityEffects/Effects/Botany/PlantMutateGasesEntityEffectSystem.cs
@@ -0,0 +1,53 @@
+using System.Linq;
+using Content.Server.Botany.Components;
+using Content.Shared.Atmos;
+using Content.Shared.EntityEffects;
+using Content.Shared.EntityEffects.Effects.Botany;
+using Robust.Shared.Random;
+
+namespace Content.Server.EntityEffects.Effects.Botany;
+
+public sealed partial class PlantMutateExudeGasesEntityEffectSystem : EntityEffectSystem
+{
+ [Dependency] private readonly IRobustRandom _random = default!;
+
+ protected override void Effect(Entity entity, ref EntityEffectEvent args)
+ {
+ if (entity.Comp.Seed == null)
+ return;
+
+ var gasses = entity.Comp.Seed.ExudeGasses;
+
+ // Add a random amount of a random gas to this gas dictionary
+ float amount = _random.NextFloat(args.Effect.MinValue, args.Effect.MaxValue);
+ var gas = _random.Pick(Enum.GetValues(typeof(Gas)).Cast().ToList());
+
+ if (!gasses.TryAdd(gas, amount))
+ {
+ gasses[gas] += amount;
+ }
+ }
+}
+
+public sealed partial class PlantMutateConsumeGasesEntityEffectSystem : EntityEffectSystem
+{
+ [Dependency] private readonly IRobustRandom _random = default!;
+
+ protected override void Effect(Entity entity, ref EntityEffectEvent args)
+ {
+ if (entity.Comp.Seed == null)
+ return;
+
+ var gasses = entity.Comp.Seed.ConsumeGasses;
+
+ // Add a random amount of a random gas to this gas dictionary
+ var amount = _random.NextFloat(args.Effect.MinValue, args.Effect.MaxValue);
+ var gas = _random.Pick(Enum.GetValues(typeof(Gas)).Cast().ToList());
+
+ if (!gasses.TryAdd(gas, amount))
+ {
+ gasses[gas] += amount;
+ }
+ }
+}
+
diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantMutateHarvestEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantMutateHarvestEntityEffectSystem.cs
new file mode 100644
index 0000000000..95d7f97bbe
--- /dev/null
+++ b/Content.Server/EntityEffects/Effects/Botany/PlantMutateHarvestEntityEffectSystem.cs
@@ -0,0 +1,25 @@
+using Content.Server.Botany;
+using Content.Server.Botany.Components;
+using Content.Shared.EntityEffects;
+using Content.Shared.EntityEffects.Effects.Botany;
+
+namespace Content.Server.EntityEffects.Effects.Botany;
+
+public sealed partial class PlantMutateHarvestEntityEffectSystem : EntityEffectSystem
+{
+ protected override void Effect(Entity entity, ref EntityEffectEvent args)
+ {
+ if (entity.Comp.Seed == null)
+ return;
+
+ switch (entity.Comp.Seed.HarvestRepeat)
+ {
+ case HarvestType.NoRepeat:
+ entity.Comp.Seed.HarvestRepeat = HarvestType.Repeat;
+ break;
+ case HarvestType.Repeat:
+ entity.Comp.Seed.HarvestRepeat = HarvestType.SelfHarvest;
+ break;
+ }
+ }
+}
diff --git a/Content.Server/EntityEffects/Effects/Botany/PlantMutateSpeciesChangeEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Botany/PlantMutateSpeciesChangeEntityEffectSystem.cs
new file mode 100644
index 0000000000..c26e1e08cf
--- /dev/null
+++ b/Content.Server/EntityEffects/Effects/Botany/PlantMutateSpeciesChangeEntityEffectSystem.cs
@@ -0,0 +1,31 @@
+using Content.Server.Botany;
+using Content.Server.Botany.Components;
+using Content.Shared.EntityEffects;
+using Content.Shared.EntityEffects.Effects.Botany;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+
+namespace Content.Server.EntityEffects.Effects.Botany;
+
+public sealed partial class PlantMutateSpeciesChangeEntityEffectSystem : EntityEffectSystem
+{
+ [Dependency] private readonly IPrototypeManager _proto = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+
+ protected override void Effect(Entity entity, ref EntityEffectEvent args)
+ {
+ if (entity.Comp.Seed == null || entity.Comp.Seed.MutationPrototypes.Count == 0)
+ return;
+
+ var targetProto = _random.Pick(entity.Comp.Seed.MutationPrototypes);
+ _proto.TryIndex(targetProto, out SeedPrototype? protoSeed);
+
+ if (protoSeed == null)
+ {
+ Log.Error($"Seed prototype could not be found: {targetProto}!");
+ return;
+ }
+
+ entity.Comp.Seed = entity.Comp.Seed.SpeciesChange(protoSeed);
+ }
+}
diff --git a/Content.Server/EntityEffects/Effects/EmoteEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/EmoteEntityEffectSystem.cs
new file mode 100644
index 0000000000..05ab857267
--- /dev/null
+++ b/Content.Server/EntityEffects/Effects/EmoteEntityEffectSystem.cs
@@ -0,0 +1,22 @@
+using Content.Server.Chat.Systems;
+using Content.Shared.EntityEffects;
+using Content.Shared.EntityEffects.Effects;
+
+namespace Content.Server.EntityEffects.Effects;
+
+///
+/// Makes this entity emote.
+///
+///
+public sealed partial class EmoteEntityEffectSystem : EntityEffectSystem
+{
+ [Dependency] private readonly ChatSystem _chat = default!;
+
+ protected override void Effect(Entity entity, ref EntityEffectEvent args)
+ {
+ if (args.Effect.ShowInChat)
+ _chat.TryEmoteWithChat(entity, args.Effect.EmoteId, ChatTransmitRange.GhostRangeLimit, forceEmote: args.Effect.Force);
+ else
+ _chat.TryEmoteWithoutChat(entity, args.Effect.EmoteId);
+ }
+}
diff --git a/Content.Server/EntityEffects/Effects/MakeSentientEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/MakeSentientEntityEffectSystem.cs
new file mode 100644
index 0000000000..c623b25857
--- /dev/null
+++ b/Content.Server/EntityEffects/Effects/MakeSentientEntityEffectSystem.cs
@@ -0,0 +1,42 @@
+using Content.Server.Ghost.Roles.Components;
+using Content.Server.Speech.Components;
+using Content.Shared.EntityEffects;
+using Content.Shared.EntityEffects.Effects;
+using Content.Shared.Mind.Components;
+
+namespace Content.Server.EntityEffects.Effects;
+
+///
+/// Makes this entity sentient. Allows ghost to take it over if it's not already occupied.
+/// Optionally also allows this entity to speak.
+///
+///
+public sealed partial class MakeSentientEntityEffectSystem : EntityEffectSystem
+{
+ protected override void Effect(Entity entity, ref EntityEffectEvent args)
+ {
+ // Let affected entities speak normally to make this effect different from, say, the "random sentience" event
+ // This also works on entities that already have a mind
+ // We call this before the mind check to allow things like player-controlled mice to be able to benefit from the effect
+ if (args.Effect.AllowSpeech)
+ {
+ RemComp(entity);
+ // TODO: Make MonkeyAccent a replacement accent and remove MonkeyAccent code-smell.
+ RemComp(entity);
+ }
+
+ // Stops from adding a ghost role to things like people who already have a mind
+ if (TryComp(entity, out var mindContainer) && mindContainer.HasMind)
+ return;
+
+ // Don't add a ghost role to things that already have ghost roles
+ if (TryComp(entity, out GhostRoleComponent? ghostRole))
+ return;
+
+ ghostRole = AddComp(entity);
+ EnsureComp(entity);
+
+ ghostRole.RoleName = entity.Comp.EntityName;
+ ghostRole.RoleDescription = Loc.GetString("ghost-role-information-cognizine-description");
+ }
+}
diff --git a/Content.Server/EntityEffects/Effects/PolymorphEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/PolymorphEntityEffectSystem.cs
new file mode 100644
index 0000000000..5f19bcc50b
--- /dev/null
+++ b/Content.Server/EntityEffects/Effects/PolymorphEntityEffectSystem.cs
@@ -0,0 +1,19 @@
+using Content.Server.Polymorph.Components;
+using Content.Server.Polymorph.Systems;
+using Content.Shared.EntityEffects;
+
+namespace Content.Server.EntityEffects.Effects;
+
+///
+/// Polymorphs this entity into another entity.
+///
+///
+public sealed partial class PolymorphEntityEffectSystem : EntityEffectSystem
+{
+ [Dependency] private readonly PolymorphSystem _polymorph = default!;
+
+ protected override void Effect(Entity entity, ref EntityEffectEvent args)
+ {
+ _polymorph.PolymorphEntity(entity, args.Effect.Prototype);
+ }
+}
diff --git a/Content.Server/EntityEffects/Effects/Solution/AreaReactionEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Solution/AreaReactionEntityEffectSystem.cs
new file mode 100644
index 0000000000..e5ef488de8
--- /dev/null
+++ b/Content.Server/EntityEffects/Effects/Solution/AreaReactionEntityEffectSystem.cs
@@ -0,0 +1,51 @@
+using Content.Server.Fluids.EntitySystems;
+using Content.Server.Spreader;
+using Content.Shared.Chemistry.Components;
+using Content.Shared.Coordinates.Helpers;
+using Content.Shared.EntityEffects;
+using Content.Shared.EntityEffects.Effects.Solution;
+using Content.Shared.Maps;
+using Robust.Shared.Audio;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Map;
+
+namespace Content.Server.EntityEffects.Effects.Solution;
+
+///
+/// This effect creates smoke at this solution's position.
+/// The amount of smoke created is modified by scale.
+///
+///
+public sealed partial class AreaReactionEntityEffectsSystem : EntityEffectSystem
+{
+ [Dependency] private readonly IMapManager _mapManager = default!;
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+ [Dependency] private readonly SharedMapSystem _map = default!;
+ [Dependency] private readonly SharedTransformSystem _xform = default!;
+ [Dependency] private readonly SmokeSystem _smoke = default!;
+ [Dependency] private readonly SpreaderSystem _spreader = default!;
+ [Dependency] private readonly TurfSystem _turf = default!;
+
+ // TODO: A sane way to make Smoke without a solution.
+ protected override void Effect(Entity entity, ref EntityEffectEvent args)
+ {
+ var xform = Transform(entity);
+ var mapCoords = _xform.GetMapCoordinates(entity);
+ var spreadAmount = (int) Math.Max(0, Math.Ceiling(args.Scale / args.Effect.OverflowThreshold));
+ var effect = args.Effect;
+
+ if (!_mapManager.TryFindGridAt(mapCoords, out var gridUid, out var grid) ||
+ !_map.TryGetTileRef(gridUid, grid, xform.Coordinates, out var tileRef))
+ return;
+
+ if (_spreader.RequiresFloorToSpread(effect.PrototypeId.ToString()) && _turf.IsSpace(tileRef))
+ return;
+
+ var coords = _map.MapToGrid(gridUid, mapCoords);
+ var ent = Spawn(args.Effect.PrototypeId, coords.SnapToGrid());
+
+ _smoke.StartSmoke(ent, entity.Comp.Solution, args.Effect.Duration, spreadAmount);
+
+ _audio.PlayPvs(args.Effect.Sound, entity, AudioParams.Default.WithVariation(0.25f));
+ }
+}
diff --git a/Content.Server/EntityEffects/Effects/Transform/ExplosionEntityEffectSystem.cs b/Content.Server/EntityEffects/Effects/Transform/ExplosionEntityEffectSystem.cs
new file mode 100644
index 0000000000..55fc120051
--- /dev/null
+++ b/Content.Server/EntityEffects/Effects/Transform/ExplosionEntityEffectSystem.cs
@@ -0,0 +1,28 @@
+using Content.Server.Explosion.EntitySystems;
+using Content.Shared.EntityEffects;
+using Content.Shared.EntityEffects.Effects.Transform;
+
+namespace Content.Server.EntityEffects.Effects.Transform;
+
+///
+/// Creates an explosion at this entity's position.
+/// Intensity is modified by scale.
+///
+///
+public sealed partial class ExplosionEntityEffectSystem : EntityEffectSystem
+{
+ [Dependency] private readonly ExplosionSystem _explosion = default!;
+
+ protected override void Effect(Entity entity, ref EntityEffectEvent args)
+ {
+ var intensity = MathF.Min(args.Effect.IntensityPerUnit * args.Scale, args.Effect.MaxTotalIntensity);
+
+ _explosion.QueueExplosion(
+ entity,
+ args.Effect.ExplosionType,
+ intensity,
+ args.Effect.IntensitySlope,
+ args.Effect.MaxIntensity,
+ args.Effect.TileBreakScale);
+ }
+}
diff --git a/Content.Server/EntityEffects/EntityEffectSystem.cs b/Content.Server/EntityEffects/EntityEffectSystem.cs
deleted file mode 100644
index 238ef4849d..0000000000
--- a/Content.Server/EntityEffects/EntityEffectSystem.cs
+++ /dev/null
@@ -1,976 +0,0 @@
-using System.Diagnostics.CodeAnalysis;
-using System.Linq;
-using Content.Server.Atmos.EntitySystems;
-using Content.Server.Body.Components;
-using Content.Server.Body.Systems;
-using Content.Server.Botany.Components;
-using Content.Server.Botany.Systems;
-using Content.Server.Botany;
-using Content.Server.Chat.Systems;
-using Content.Server.Emp;
-using Content.Server.Explosion.EntitySystems;
-using Content.Server.Fluids.EntitySystems;
-using Content.Server.Ghost.Roles.Components;
-using Content.Server.Polymorph.Components;
-using Content.Server.Polymorph.Systems;
-using Content.Server.Speech.Components;
-using Content.Server.Spreader;
-using Content.Server.Temperature.Components;
-using Content.Server.Temperature.Systems;
-using Content.Server.Zombies;
-using Content.Shared.Atmos;
-using Content.Shared.Atmos.Components;
-using Content.Shared.Body.Components;
-using Content.Shared.Coordinates.Helpers;
-using Content.Shared.EntityEffects.EffectConditions;
-using Content.Shared.EntityEffects.Effects.PlantMetabolism;
-using Content.Shared.EntityEffects.Effects;
-using Content.Shared.EntityEffects;
-using Content.Shared.Flash;
-using Content.Shared.Maps;
-using Content.Shared.Medical;
-using Content.Shared.Mind.Components;
-using Content.Shared.Popups;
-using Content.Shared.Random;
-using Content.Shared.Traits.Assorted;
-using Content.Shared.Zombies;
-using Robust.Server.GameObjects;
-using Robust.Shared.Audio;
-using Robust.Shared.Audio.Systems;
-using Robust.Shared.Map;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Random;
-
-using TemperatureCondition = Content.Shared.EntityEffects.EffectConditions.Temperature; // disambiguate the namespace
-using PolymorphEffect = Content.Shared.EntityEffects.Effects.Polymorph;
-
-namespace Content.Server.EntityEffects;
-
-public sealed class EntityEffectSystem : EntitySystem
-{
- private static readonly ProtoId RandomPickBotanyReagent = "RandomPickBotanyReagent";
-
- [Dependency] private readonly AtmosphereSystem _atmosphere = default!;
- [Dependency] private readonly BloodstreamSystem _bloodstream = default!;
- [Dependency] private readonly ChatSystem _chat = default!;
- [Dependency] private readonly EmpSystem _emp = default!;
- [Dependency] private readonly ExplosionSystem _explosion = default!;
- [Dependency] private readonly FlammableSystem _flammable = default!;
- [Dependency] private readonly SharedFlashSystem _flash = default!;
- [Dependency] private readonly IMapManager _mapManager = default!;
- [Dependency] private readonly IPrototypeManager _protoManager = default!;
- [Dependency] private readonly IRobustRandom _random = default!;
- [Dependency] private readonly SharedMapSystem _map = default!;
- [Dependency] private readonly MutationSystem _mutation = default!;
- [Dependency] private readonly NarcolepsySystem _narcolepsy = default!;
- [Dependency] private readonly PlantHolderSystem _plantHolder = default!;
- [Dependency] private readonly PolymorphSystem _polymorph = default!;
- [Dependency] private readonly RespiratorSystem _respirator = default!;
- [Dependency] private readonly SharedAudioSystem _audio = default!;
- [Dependency] private readonly SharedPointLightSystem _pointLight = default!;
- [Dependency] private readonly SharedPopupSystem _popup = default!;
- [Dependency] private readonly SmokeSystem _smoke = default!;
- [Dependency] private readonly SpreaderSystem _spreader = default!;
- [Dependency] private readonly TemperatureSystem _temperature = default!;
- [Dependency] private readonly SharedTransformSystem _xform = default!;
- [Dependency] private readonly VomitSystem _vomit = default!;
- [Dependency] private readonly TurfSystem _turf = default!;
-
- public override void Initialize()
- {
- base.Initialize();
-
- SubscribeLocalEvent>(OnCheckTemperature);
- SubscribeLocalEvent>(OnCheckBreathing);
- SubscribeLocalEvent>(OnCheckOrganType);
- SubscribeLocalEvent>(OnExecutePlantAdjustHealth);
- SubscribeLocalEvent>(OnExecutePlantAdjustMutationLevel);
- SubscribeLocalEvent>(OnExecutePlantAdjustMutationMod);
- SubscribeLocalEvent>(OnExecutePlantAdjustNutrition);
- SubscribeLocalEvent>(OnExecutePlantAdjustPests);
- SubscribeLocalEvent>(OnExecutePlantAdjustPotency);
- SubscribeLocalEvent>(OnExecutePlantAdjustToxins);
- SubscribeLocalEvent>(OnExecutePlantAdjustWater);
- SubscribeLocalEvent>(OnExecutePlantAdjustWeeds);
- SubscribeLocalEvent>(OnExecutePlantAffectGrowth);
- SubscribeLocalEvent>(OnExecutePlantChangeStat);
- SubscribeLocalEvent>(OnExecutePlantCryoxadone);
- SubscribeLocalEvent>(OnExecutePlantDestroySeeds);
- SubscribeLocalEvent>(OnExecutePlantDiethylamine);
- SubscribeLocalEvent>(OnExecutePlantPhalanximine);
- SubscribeLocalEvent>(OnExecutePlantRestoreSeeds);
- SubscribeLocalEvent>(OnExecuteRobustHarvest);
- SubscribeLocalEvent>(OnExecuteAdjustTemperature);
- SubscribeLocalEvent>(OnExecuteAreaReactionEffect);
- SubscribeLocalEvent>(OnExecuteCauseZombieInfection);
- SubscribeLocalEvent>(OnExecuteChemCleanBloodstream);
- SubscribeLocalEvent>(OnExecuteChemVomit);
- SubscribeLocalEvent>(OnExecuteCreateEntityReactionEffect);
- SubscribeLocalEvent>(OnExecuteCreateGas);
- SubscribeLocalEvent>(OnExecuteCureZombieInfection);
- SubscribeLocalEvent>(OnExecuteEmote);
- SubscribeLocalEvent>(OnExecuteEmpReactionEffect);
- SubscribeLocalEvent>(OnExecuteExplosionReactionEffect);
- SubscribeLocalEvent>(OnExecuteFlammableReaction);
- SubscribeLocalEvent>(OnExecuteFlashReactionEffect);
- SubscribeLocalEvent>(OnExecuteIgnite);
- SubscribeLocalEvent>(OnExecuteMakeSentient);
- SubscribeLocalEvent>(OnExecuteModifyBleedAmount);
- SubscribeLocalEvent>(OnExecuteModifyBloodLevel);
- SubscribeLocalEvent>(OnExecuteModifyLungGas);
- SubscribeLocalEvent>(OnExecuteOxygenate);
- SubscribeLocalEvent>(OnExecutePlantMutateChemicals);
- SubscribeLocalEvent>(OnExecutePlantMutateConsumeGasses);
- SubscribeLocalEvent>(OnExecutePlantMutateExudeGasses);
- SubscribeLocalEvent>(OnExecutePlantMutateHarvest);
- SubscribeLocalEvent>(OnExecutePlantSpeciesChange);
- SubscribeLocalEvent>(OnExecutePolymorph);
- SubscribeLocalEvent>(OnExecuteResetNarcolepsy);
- }
-
- private void OnCheckTemperature(ref CheckEntityEffectConditionEvent args)
- {
- args.Result = false;
- if (TryComp(args.Args.TargetEntity, out TemperatureComponent? temp))
- {
- if (temp.CurrentTemperature >= args.Condition.Min && temp.CurrentTemperature <= args.Condition.Max)
- args.Result = true;
- }
- }
-
- private void OnCheckBreathing(ref CheckEntityEffectConditionEvent args)
- {
- if (!TryComp(args.Args.TargetEntity, out RespiratorComponent? respiratorComp))
- {
- args.Result = !args.Condition.IsBreathing;
- return;
- }
-
- var breathingState = _respirator.IsBreathing((args.Args.TargetEntity, respiratorComp));
- args.Result = args.Condition.IsBreathing == breathingState;
- }
-
- private void OnCheckOrganType(ref CheckEntityEffectConditionEvent args)
- {
- if (args.Args is EntityEffectReagentArgs reagentArgs)
- {
- if (reagentArgs.OrganEntity == null)
- {
- args.Result = false;
- return;
- }
-
- args.Result = OrganCondition(args.Condition, reagentArgs.OrganEntity.Value);
- return;
- }
-
- // TODO: Someone needs to figure out how to do this for non-reagent effects.
- throw new NotImplementedException();
- }
-
- public bool OrganCondition(OrganType condition, Entity metabolizer)
- {
- metabolizer.Comp ??= EntityManager.GetComponentOrNull(metabolizer.Owner);
- if (metabolizer.Comp != null
- && metabolizer.Comp.MetabolizerTypes != null
- && metabolizer.Comp.MetabolizerTypes.Contains(condition.Type))
- return condition.ShouldHave;
- return !condition.ShouldHave;
- }
-
- ///
- /// Checks if the plant holder can metabolize the reagent or not. Checks if it has an alive plant by default.
- ///
- /// The entity holding the plant
- /// The plant holder component
- /// The entity manager
- /// Whether to check if it has an alive plant or not
- ///
- private bool CanMetabolizePlant(EntityUid plantHolder, [NotNullWhen(true)] out PlantHolderComponent? plantHolderComponent,
- bool mustHaveAlivePlant = true, bool mustHaveMutableSeed = false)
- {
- plantHolderComponent = null;
-
- if (!TryComp(plantHolder, out plantHolderComponent))
- return false;
-
- if (mustHaveAlivePlant && (plantHolderComponent.Seed == null || plantHolderComponent.Dead))
- return false;
-
- if (mustHaveMutableSeed && (plantHolderComponent.Seed == null || plantHolderComponent.Seed.Immutable))
- return false;
-
- return true;
- }
-
- private void OnExecutePlantAdjustHealth(ref ExecuteEntityEffectEvent args)
- {
- if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp))
- return;
-
- plantHolderComp.Health += args.Effect.Amount;
- _plantHolder.CheckHealth(args.Args.TargetEntity, plantHolderComp);
- }
-
- private void OnExecutePlantAdjustMutationLevel(ref ExecuteEntityEffectEvent args)
- {
- if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp))
- return;
-
- plantHolderComp.MutationLevel += args.Effect.Amount * plantHolderComp.MutationMod;
- }
-
- private void OnExecutePlantAdjustMutationMod(ref ExecuteEntityEffectEvent args)
- {
- if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp))
- return;
-
- plantHolderComp.MutationMod += args.Effect.Amount;
- }
-
- private void OnExecutePlantAdjustNutrition(ref ExecuteEntityEffectEvent args)
- {
- if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp, mustHaveAlivePlant: false))
- return;
-
- _plantHolder.AdjustNutrient(args.Args.TargetEntity, args.Effect.Amount, plantHolderComp);
- }
-
- private void OnExecutePlantAdjustPests(ref ExecuteEntityEffectEvent args)
- {
- if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp))
- return;
-
- plantHolderComp.PestLevel += args.Effect.Amount;
- }
-
- private void OnExecutePlantAdjustPotency(ref ExecuteEntityEffectEvent args)
- {
- if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp))
- return;
-
- if (plantHolderComp.Seed == null)
- return;
-
- _plantHolder.EnsureUniqueSeed(args.Args.TargetEntity, plantHolderComp);
- plantHolderComp.Seed.Potency = Math.Max(plantHolderComp.Seed.Potency + args.Effect.Amount, 1);
- }
-
- private void OnExecutePlantAdjustToxins(ref ExecuteEntityEffectEvent args)
- {
- if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp))
- return;
-
- plantHolderComp.Toxins += args.Effect.Amount;
- }
-
- private void OnExecutePlantAdjustWater(ref ExecuteEntityEffectEvent args)
- {
- if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp, mustHaveAlivePlant: false))
- return;
-
- _plantHolder.AdjustWater(args.Args.TargetEntity, args.Effect.Amount, plantHolderComp);
- }
-
- private void OnExecutePlantAdjustWeeds(ref ExecuteEntityEffectEvent args)
- {
- if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp))
- return;
-
- plantHolderComp.WeedLevel += args.Effect.Amount;
- }
-
- private void OnExecutePlantAffectGrowth(ref ExecuteEntityEffectEvent args)
- {
- if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp))
- return;
-
- _plantHolder.AffectGrowth(args.Args.TargetEntity, (int) args.Effect.Amount, plantHolderComp);
- }
-
- // Mutate reference 'val' between 'min' and 'max' by pretending the value
- // is representable by a thermometer code with 'bits' number of bits and
- // randomly flipping some of them.
- private void MutateFloat(ref float val, float min, float max, int bits)
- {
- if (min == max)
- {
- val = min;
- return;
- }
-
- // Starting number of bits that are high, between 0 and bits.
- // In other words, it's val mapped linearly from range [min, max] to range [0, bits], and then rounded.
- int valInt = (int)MathF.Round((val - min) / (max - min) * bits);
- // val may be outside the range of min/max due to starting prototype values, so clamp.
- valInt = Math.Clamp(valInt, 0, bits);
-
- // Probability that the bit flip increases n.
- // The higher the current value is, the lower the probability of increasing value is, and the higher the probability of decreasive it it.
- // In other words, it tends to go to the middle.
- float probIncrease = 1 - (float)valInt / bits;
- int valIntMutated;
- if (_random.Prob(probIncrease))
- {
- valIntMutated = valInt + 1;
- }
- else
- {
- valIntMutated = valInt - 1;
- }
-
- // Set value based on mutated thermometer code.
- float valMutated = Math.Clamp((float)valIntMutated / bits * (max - min) + min, min, max);
- val = valMutated;
- }
-
- private void MutateInt(ref int val, int min, int max, int bits)
- {
- if (min == max)
- {
- val = min;
- return;
- }
-
- // Starting number of bits that are high, between 0 and bits.
- // In other words, it's val mapped linearly from range [min, max] to range [0, bits], and then rounded.
- int valInt = (int)MathF.Round((val - min) / (max - min) * bits);
- // val may be outside the range of min/max due to starting prototype values, so clamp.
- valInt = Math.Clamp(valInt, 0, bits);
-
- // Probability that the bit flip increases n.
- // The higher the current value is, the lower the probability of increasing value is, and the higher the probability of decreasing it.
- // In other words, it tends to go to the middle.
- float probIncrease = 1 - (float)valInt / bits;
- int valMutated;
- if (_random.Prob(probIncrease))
- {
- valMutated = val + 1;
- }
- else
- {
- valMutated = val - 1;
- }
-
- valMutated = Math.Clamp(valMutated, min, max);
- val = valMutated;
- }
-
- private void OnExecutePlantChangeStat(ref ExecuteEntityEffectEvent args)
- {
- if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp))
- return;
-
- if (plantHolderComp.Seed == null)
- return;
-
- var member = plantHolderComp.Seed.GetType().GetField(args.Effect.TargetValue);
-
- if (member == null)
- {
- _mutation.Log.Error(args.Effect.GetType().Name + " Error: Member " + args.Effect.TargetValue + " not found on " + plantHolderComp.Seed.GetType().Name + ". Did you misspell it?");
- return;
- }
-
- var currentValObj = member.GetValue(plantHolderComp.Seed);
- if (currentValObj == null)
- return;
-
- if (member.FieldType == typeof(float))
- {
- var floatVal = (float)currentValObj;
- MutateFloat(ref floatVal, args.Effect.MinValue, args.Effect.MaxValue, args.Effect.Steps);
- member.SetValue(plantHolderComp.Seed, floatVal);
- }
- else if (member.FieldType == typeof(int))
- {
- var intVal = (int)currentValObj;
- MutateInt(ref intVal, (int)args.Effect.MinValue, (int)args.Effect.MaxValue, args.Effect.Steps);
- member.SetValue(plantHolderComp.Seed, intVal);
- }
- else if (member.FieldType == typeof(bool))
- {
- var boolVal = (bool)currentValObj;
- boolVal = !boolVal;
- member.SetValue(plantHolderComp.Seed, boolVal);
- }
- }
-
- private void OnExecutePlantCryoxadone(ref ExecuteEntityEffectEvent args)
- {
- if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp))
- return;
-
- var deviation = 0;
- var seed = plantHolderComp.Seed;
- if (seed == null)
- return;
- if (plantHolderComp.Age > seed.Maturation)
- deviation = (int) Math.Max(seed.Maturation - 1, plantHolderComp.Age - _random.Next(7, 10));
- else
- deviation = (int) (seed.Maturation / seed.GrowthStages);
- plantHolderComp.Age -= deviation;
- plantHolderComp.LastProduce = plantHolderComp.Age;
- plantHolderComp.SkipAging++;
- plantHolderComp.ForceUpdate = true;
- }
-
- private void OnExecutePlantDestroySeeds(ref ExecuteEntityEffectEvent args)
- {
- if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp, mustHaveMutableSeed: true))
- return;
-
- if (plantHolderComp.Seed!.Seedless == false)
- {
- _plantHolder.EnsureUniqueSeed(args.Args.TargetEntity, plantHolderComp);
- _popup.PopupEntity(
- Loc.GetString("botany-plant-seedsdestroyed"),
- args.Args.TargetEntity,
- PopupType.SmallCaution
- );
- plantHolderComp.Seed.Seedless = true;
- }
- }
-
- private void OnExecutePlantDiethylamine(ref ExecuteEntityEffectEvent args)
- {
- if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp, mustHaveMutableSeed: true))
- return;
-
- if (_random.Prob(0.1f))
- {
- _plantHolder.EnsureUniqueSeed(args.Args.TargetEntity, plantHolderComp);
- plantHolderComp.Seed!.Lifespan++;
- }
-
- if (_random.Prob(0.1f))
- {
- _plantHolder.EnsureUniqueSeed(args.Args.TargetEntity, plantHolderComp);
- plantHolderComp.Seed!.Endurance++;
- }
- }
-
- private void OnExecutePlantPhalanximine(ref ExecuteEntityEffectEvent args)
- {
- if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp, mustHaveMutableSeed: true))
- return;
-
- plantHolderComp.Seed!.Viable = true;
- }
-
- private void OnExecutePlantRestoreSeeds(ref ExecuteEntityEffectEvent args)
- {
- if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp, mustHaveMutableSeed: true))
- return;
-
- if (plantHolderComp.Seed!.Seedless)
- {
- _plantHolder.EnsureUniqueSeed(args.Args.TargetEntity, plantHolderComp);
- _popup.PopupEntity(Loc.GetString("botany-plant-seedsrestored"), args.Args.TargetEntity);
- plantHolderComp.Seed.Seedless = false;
- }
- }
-
- private void OnExecuteRobustHarvest(ref ExecuteEntityEffectEvent args)
- {
- if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp))
- return;
-
- if (plantHolderComp.Seed == null)
- return;
-
- if (plantHolderComp.Seed.Potency < args.Effect.PotencyLimit)
- {
- _plantHolder.EnsureUniqueSeed(args.Args.TargetEntity, plantHolderComp);
- plantHolderComp.Seed.Potency = Math.Min(plantHolderComp.Seed.Potency + args.Effect.PotencyIncrease, args.Effect.PotencyLimit);
-
- if (plantHolderComp.Seed.Potency > args.Effect.PotencySeedlessThreshold)
- {
- plantHolderComp.Seed.Seedless = true;
- }
- }
- else if (plantHolderComp.Seed.Yield > 1 && _random.Prob(0.1f))
- {
- // Too much of a good thing reduces yield
- _plantHolder.EnsureUniqueSeed(args.Args.TargetEntity, plantHolderComp);
- plantHolderComp.Seed.Yield--;
- }
- }
-
- private void OnExecuteAdjustTemperature(ref ExecuteEntityEffectEvent args)
- {
- if (TryComp(args.Args.TargetEntity, out TemperatureComponent? temp))
- {
- var amount = args.Effect.Amount;
-
- if (args.Args is EntityEffectReagentArgs reagentArgs)
- {
- amount *= reagentArgs.Scale.Float();
- }
-
- _temperature.ChangeHeat(args.Args.TargetEntity, amount, true, temp);
- }
- }
-
- private void OnExecuteAreaReactionEffect(ref ExecuteEntityEffectEvent args)
- {
- if (args.Args is EntityEffectReagentArgs reagentArgs)
- {
- if (reagentArgs.Source == null)
- return;
-
- var spreadAmount = (int) Math.Max(0, Math.Ceiling((reagentArgs.Quantity / args.Effect.OverflowThreshold).Float()));
- var splitSolution = reagentArgs.Source.SplitSolution(reagentArgs.Source.Volume);
- var transform = Comp(reagentArgs.TargetEntity);
- var mapCoords = _xform.GetMapCoordinates(reagentArgs.TargetEntity, xform: transform);
-
- if (!_mapManager.TryFindGridAt(mapCoords, out var gridUid, out var grid) ||
- !_map.TryGetTileRef(gridUid, grid, transform.Coordinates, out var tileRef))
- {
- return;
- }
-
- if (_spreader.RequiresFloorToSpread(args.Effect.PrototypeId) && _turf.IsSpace(tileRef))
- return;
-
- var coords = _map.MapToGrid(gridUid, mapCoords);
- var ent = Spawn(args.Effect.PrototypeId, coords.SnapToGrid());
-
- _smoke.StartSmoke(ent, splitSolution, args.Effect.Duration, spreadAmount);
-
- _audio.PlayPvs(args.Effect.Sound, reagentArgs.TargetEntity, AudioParams.Default.WithVariation(0.25f));
- return;
- }
-
- // TODO: Someone needs to figure out how to do this for non-reagent effects.
- throw new NotImplementedException();
- }
-
- private void OnExecuteCauseZombieInfection(ref ExecuteEntityEffectEvent args)
- {
- EnsureComp(args.Args.TargetEntity);
- EnsureComp(args.Args.TargetEntity);
- }
-
- private void OnExecuteChemCleanBloodstream(ref ExecuteEntityEffectEvent args)
- {
- var cleanseRate = args.Effect.CleanseRate;
- if (args.Args is EntityEffectReagentArgs reagentArgs)
- {
- if (reagentArgs.Source == null || reagentArgs.Reagent == null)
- return;
-
- cleanseRate *= reagentArgs.Scale.Float();
- _bloodstream.FlushChemicals(args.Args.TargetEntity, reagentArgs.Reagent, cleanseRate);
- }
- else
- {
- _bloodstream.FlushChemicals(args.Args.TargetEntity, null, cleanseRate);
- }
- }
-
- private void OnExecuteChemVomit(ref ExecuteEntityEffectEvent args)
- {
- if (args.Args is EntityEffectReagentArgs reagentArgs)
- if (reagentArgs.Scale != 1f)
- return;
-
- _vomit.Vomit(args.Args.TargetEntity, args.Effect.ThirstAmount, args.Effect.HungerAmount);
- }
-
- private void OnExecuteCreateEntityReactionEffect(ref ExecuteEntityEffectEvent args)
- {
- var transform = Comp(args.Args.TargetEntity);
- var quantity = (int)args.Effect.Number;
- if (args.Args is EntityEffectReagentArgs reagentArgs)
- quantity *= reagentArgs.Quantity.Int();
-
- for (var i = 0; i < quantity; i++)
- {
- var uid = Spawn(args.Effect.Entity, _xform.GetMapCoordinates(args.Args.TargetEntity, xform: transform));
- _xform.AttachToGridOrMap(uid);
-
- // TODO figure out how to properly spawn inside of containers
- // e.g. cheese:
- // if the user is holding a bowl milk & enzyme, should drop to floor, not attached to the user.
- // if reaction happens in a backpack, should insert cheese into backpack.
- // --> if it doesn't fit, iterate through parent storage until it attaches to the grid (again, DON'T attach to players).
- // if the reaction happens INSIDE a stomach? the bloodstream? I have no idea how to handle that.
- // presumably having cheese materialize inside of your blood would have "disadvantages".
- }
- }
-
- private void OnExecuteCreateGas(ref ExecuteEntityEffectEvent args)
- {
- var tileMix = _atmosphere.GetContainingMixture(args.Args.TargetEntity, false, true);
-
- if (tileMix != null)
- {
- if (args.Args is EntityEffectReagentArgs reagentArgs)
- {
- tileMix.AdjustMoles(args.Effect.Gas, reagentArgs.Quantity.Float() * args.Effect.Multiplier);
- }
- else
- {
- tileMix.AdjustMoles(args.Effect.Gas, args.Effect.Multiplier);
- }
- }
- }
-
- private void OnExecuteCureZombieInfection(ref ExecuteEntityEffectEvent args)
- {
- if (HasComp(args.Args.TargetEntity))
- return;
-
- RemComp(args.Args.TargetEntity);
- RemComp(args.Args.TargetEntity);
-
- if (args.Effect.Innoculate)
- {
- EnsureComp(args.Args.TargetEntity);
- }
- }
-
- private void OnExecuteEmote(ref ExecuteEntityEffectEvent args)
- {
- if (args.Effect.EmoteId == null)
- return;
-
- if (args.Effect.ShowInChat)
- _chat.TryEmoteWithChat(args.Args.TargetEntity, args.Effect.EmoteId, ChatTransmitRange.GhostRangeLimit, forceEmote: args.Effect.Force);
- else
- _chat.TryEmoteWithoutChat(args.Args.TargetEntity, args.Effect.EmoteId);
- }
-
- private void OnExecuteEmpReactionEffect(ref ExecuteEntityEffectEvent args)
- {
- var transform = Comp(args.Args.TargetEntity);
-
- var range = args.Effect.EmpRangePerUnit;
-
- if (args.Args is EntityEffectReagentArgs reagentArgs)
- {
- range = MathF.Min((float) (reagentArgs.Quantity * args.Effect.EmpRangePerUnit), args.Effect.EmpMaxRange);
- }
-
- _emp.EmpPulse(_xform.GetMapCoordinates(args.Args.TargetEntity, xform: transform),
- range,
- args.Effect.EnergyConsumption,
- args.Effect.DisableDuration);
- }
-
- private void OnExecuteExplosionReactionEffect(ref ExecuteEntityEffectEvent args)
- {
- var intensity = args.Effect.IntensityPerUnit;
-
- if (args.Args is EntityEffectReagentArgs reagentArgs)
- {
- intensity = MathF.Min((float) reagentArgs.Quantity * args.Effect.IntensityPerUnit, args.Effect.MaxTotalIntensity);
- }
-
- _explosion.QueueExplosion(
- args.Args.TargetEntity,
- args.Effect.ExplosionType,
- intensity,
- args.Effect.IntensitySlope,
- args.Effect.MaxIntensity,
- args.Effect.TileBreakScale);
- }
-
- private void OnExecuteFlammableReaction(ref ExecuteEntityEffectEvent args)
- {
- if (!TryComp(args.Args.TargetEntity, out FlammableComponent? flammable))
- return;
-
- // Sets the multiplier for FireStacks to MultiplierOnExisting is 0 or greater and target already has FireStacks
- var multiplier = flammable.FireStacks != 0f && args.Effect.MultiplierOnExisting >= 0 ? args.Effect.MultiplierOnExisting : args.Effect.Multiplier;
- var quantity = 1f;
- if (args.Args is EntityEffectReagentArgs reagentArgs)
- {
- quantity = reagentArgs.Quantity.Float();
- _flammable.AdjustFireStacks(args.Args.TargetEntity, quantity * multiplier, flammable);
- if (reagentArgs.Reagent != null)
- reagentArgs.Source?.RemoveReagent(reagentArgs.Reagent.ID, reagentArgs.Quantity);
- }
- else
- {
- _flammable.AdjustFireStacks(args.Args.TargetEntity, multiplier, flammable);
- }
- }
-
- private void OnExecuteFlashReactionEffect(ref ExecuteEntityEffectEvent args)
- {
- var transform = Comp(args.Args.TargetEntity);
-
- var range = 1f;
-
- if (args.Args is EntityEffectReagentArgs reagentArgs)
- range = MathF.Min((float)(reagentArgs.Quantity * args.Effect.RangePerUnit), args.Effect.MaxRange);
-
- _flash.FlashArea(
- args.Args.TargetEntity,
- null,
- range,
- args.Effect.Duration,
- slowTo: args.Effect.SlowTo,
- sound: args.Effect.Sound);
-
- if (args.Effect.FlashEffectPrototype == null)
- return;
-
- var uid = EntityManager.SpawnEntity(args.Effect.FlashEffectPrototype, _xform.GetMapCoordinates(transform));
- _xform.AttachToGridOrMap(uid);
-
- if (!TryComp(uid, out var pointLightComp))
- return;
-
- _pointLight.SetRadius(uid, MathF.Max(1.1f, range), pointLightComp);
- }
-
- private void OnExecuteIgnite(ref ExecuteEntityEffectEvent args)
- {
- if (!TryComp(args.Args.TargetEntity, out FlammableComponent? flammable))
- return;
-
- if (args.Args is EntityEffectReagentArgs reagentArgs)
- {
- _flammable.Ignite(reagentArgs.TargetEntity, reagentArgs.OrganEntity ?? reagentArgs.TargetEntity, flammable: flammable);
- }
- else
- {
- _flammable.Ignite(args.Args.TargetEntity, args.Args.TargetEntity, flammable: flammable);
- }
- }
-
- private void OnExecuteMakeSentient(ref ExecuteEntityEffectEvent args)
- {
- var uid = args.Args.TargetEntity;
-
- // Let affected entities speak normally to make this effect different from, say, the "random sentience" event
- // This also works on entities that already have a mind
- // We call this before the mind check to allow things like player-controlled mice to be able to benefit from the effect
- RemComp(uid);
- RemComp(uid);
-
- // Stops from adding a ghost role to things like people who already have a mind
- if (TryComp(uid, out var mindContainer) && mindContainer.HasMind)
- {
- return;
- }
-
- // Don't add a ghost role to things that already have ghost roles
- if (TryComp(uid, out GhostRoleComponent? ghostRole))
- {
- return;
- }
-
- ghostRole = AddComp(uid);
- EnsureComp(uid);
-
- var entityData = Comp(uid);
- ghostRole.RoleName = entityData.EntityName;
- ghostRole.RoleDescription = Loc.GetString("ghost-role-information-cognizine-description");
- }
-
- private void OnExecuteModifyBleedAmount(ref ExecuteEntityEffectEvent args)
- {
- if (TryComp(args.Args.TargetEntity, out var blood))
- {
- var amt = args.Effect.Amount;
- if (args.Args is EntityEffectReagentArgs reagentArgs) {
- if (args.Effect.Scaled)
- amt *= reagentArgs.Quantity.Float();
- amt *= reagentArgs.Scale.Float();
- }
-
- _bloodstream.TryModifyBleedAmount((args.Args.TargetEntity, blood), amt);
- }
- }
-
- private void OnExecuteModifyBloodLevel(ref ExecuteEntityEffectEvent args)
- {
- if (TryComp(args.Args.TargetEntity, out var blood))
- {
- var amt = args.Effect.Amount;
- if (args.Args is EntityEffectReagentArgs reagentArgs)
- {
- if (args.Effect.Scaled)
- amt *= reagentArgs.Quantity;
- amt *= reagentArgs.Scale;
- }
-
- _bloodstream.TryModifyBloodLevel((args.Args.TargetEntity, blood), amt);
- }
- }
-
- private void OnExecuteModifyLungGas(ref ExecuteEntityEffectEvent args)
- {
- LungComponent? lung;
- float amount = 1f;
-
- if (args.Args is EntityEffectReagentArgs reagentArgs)
- {
- if (!TryComp(reagentArgs.OrganEntity, out var organLung))
- return;
- lung = organLung;
- amount = reagentArgs.Quantity.Float();
- }
- else
- {
- if (!TryComp(args.Args.TargetEntity, out var organLung)) //Likely needs to be modified to ensure it works correctly
- return;
- lung = organLung;
- }
-
- if (lung != null)
- {
- foreach (var (gas, ratio) in args.Effect.Ratios)
- {
- var quantity = ratio * amount / Atmospherics.BreathMolesToReagentMultiplier;
- if (quantity < 0)
- quantity = Math.Max(quantity, -lung.Air[(int) gas]);
- lung.Air.AdjustMoles(gas, quantity);
- }
- }
- }
-
- private void OnExecuteOxygenate(ref ExecuteEntityEffectEvent args)
- {
- var multiplier = 1f;
- if (args.Args is EntityEffectReagentArgs reagentArgs)
- {
- multiplier = reagentArgs.Quantity.Float();
- }
-
- if (TryComp