Entity effects ECS refactor (#40580)

* LOCKED THE FUCK IN

* Forgot this little fella

* Crying

* All entity effects ported, needs cleanup still

* Commit

* HEHEHEHAW

* Shelve for now

* fixe

* Big

* First big chunk of changes

* Big if true

* Commit

* IT BUILDS!!!

* Fix LINTER fails

* Cleanup

* Scale working, cut down on some evil code

* Delete old Entity Effects

* Accidentally breaking shit by fixing bugs

* Fix a bunch of effects not working

* Fix reagent thresholds

* Update damage

* Wait don't change the gas metabolisms A

* Cleanup

* more fixes

* Eh

* Misc fixes and jank

* Remove two things, add bullshit, change condition to inverted

* Remove unused "Shared" system structure

* Namespace fix

* merge conflicts/cleanup

* More fixes

* Guidebook text begins

* Shelve

* Push

* More shit to push

* Fix

* Fix merg conflicts

* BLOOD FOR THE BLOOD GOD!!!

* Mild cleanup and lists

* Fix localization and comments

* Shuffle localization around a bit.

* All done?

* Nearly everything

* Is this the end?

* Whoops forgot to remove that TODO

* Get rid of some warnings for good measure...

* It's done

* Should make those virtual in case we want to override them tbqh...

* Update Content.Shared/EntityEffects/Effects/Botany/PlantAttributes/PlantDestroySeeds.cs

Co-authored-by: Pok <113675512+Pok27@users.noreply.github.com>

* Fix test fails real

* Add to codeowners

* Documentation to everything

* Forgot to push whoops

* Standardize Condition names

* Fix up metabolism a little as a treat

* review

* add IsServer checks

---------

Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
Co-authored-by: Pok <113675512+Pok27@users.noreply.github.com>
This commit is contained in:
Princess Cheeseballs
2025-10-12 14:23:42 -07:00
committed by GitHub
parent 4d316ae553
commit 4059c29ebc
289 changed files with 5635 additions and 4918 deletions

1
.github/CODEOWNERS vendored
View File

@@ -28,6 +28,7 @@
/Content.*/Stunnable/ @Princess-Cheeseballs /Content.*/Stunnable/ @Princess-Cheeseballs
/Content.*/Nutrition/ @Princess-Cheeseballs /Content.*/Nutrition/ @Princess-Cheeseballs
/Content.*/EntityEffects @Princess-Cheeseballs @sowelipililimute
# SKREEEE # SKREEEE
/Content.*.Database/ @PJB3005 @DrSmugleaf /Content.*.Database/ @PJB3005 @DrSmugleaf

View File

@@ -0,0 +1,8 @@
using Content.Shared.Temperature.Systems;
namespace Content.Client.Temperature.Systems;
/// <summary>
/// This exists so <see cref="SharedTemperatureSystem"/> runs on client/>
/// </summary>
public sealed class TemperatureSystem : SharedTemperatureSystem;

View File

@@ -1,7 +1,6 @@
using Content.Server.Administration.Logs; using Content.Server.Administration.Logs;
using Content.Server.Atmos.Components; using Content.Server.Atmos.Components;
using Content.Server.Stunnable; using Content.Server.Stunnable;
using Content.Server.Temperature.Components;
using Content.Server.Temperature.Systems; using Content.Server.Temperature.Systems;
using Content.Server.Damage.Components; using Content.Server.Damage.Components;
using Content.Shared.ActionBlocker; using Content.Shared.ActionBlocker;
@@ -24,6 +23,7 @@ using Content.Shared.Toggleable;
using Content.Shared.Weapons.Melee.Events; using Content.Shared.Weapons.Melee.Events;
using Content.Shared.FixedPoint; using Content.Shared.FixedPoint;
using Content.Shared.Hands; using Content.Shared.Hands;
using Content.Shared.Temperature.Components;
using Robust.Server.Audio; using Robust.Server.Audio;
using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Events; using Robust.Shared.Physics.Events;

View File

@@ -1,9 +1,9 @@
using Content.Server.Atmos.EntitySystems; using Content.Server.Atmos.EntitySystems;
using Content.Server.Temperature.Components;
using Content.Shared.Atmos; using Content.Shared.Atmos;
using Content.Shared.Atmos.Rotting; using Content.Shared.Atmos.Rotting;
using Content.Shared.Body.Events; using Content.Shared.Body.Events;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.Temperature.Components;
using Robust.Server.Containers; using Robust.Server.Containers;
using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Components;
using Robust.Shared.Timing; using Robust.Shared.Timing;

View File

@@ -10,13 +10,13 @@ namespace Content.Server.Body.Components
/// <summary> /// <summary>
/// Handles metabolizing various reagents with given effects. /// Handles metabolizing various reagents with given effects.
/// </summary> /// </summary>
[RegisterComponent, Access(typeof(MetabolizerSystem))] [RegisterComponent, AutoGenerateComponentPause, Access(typeof(MetabolizerSystem))]
public sealed partial class MetabolizerComponent : Component public sealed partial class MetabolizerComponent : Component
{ {
/// <summary> /// <summary>
/// The next time that reagents will be metabolized. /// The next time that reagents will be metabolized.
/// </summary> /// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] [DataField, AutoPausedField]
public TimeSpan NextUpdate; public TimeSpan NextUpdate;
/// <summary> /// <summary>

View File

@@ -1,14 +1,18 @@
using Content.Server.Body.Components; using Content.Server.Body.Components;
using Content.Shared.Administration.Logs;
using Content.Shared.Body.Events; using Content.Shared.Body.Events;
using Content.Shared.Body.Organ; using Content.Shared.Body.Organ;
using Content.Shared.Body.Prototypes;
using Content.Shared.Body.Systems; using Content.Shared.Body.Systems;
using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Components.SolutionManager; using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reagent; 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;
using Content.Shared.EntityEffects.Effects.Body;
using Content.Shared.EntityEffects.Effects.Solution;
using Content.Shared.FixedPoint; using Content.Shared.FixedPoint;
using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems; using Content.Shared.Mobs.Systems;
@@ -17,210 +21,258 @@ using Robust.Shared.Prototypes;
using Robust.Shared.Random; using Robust.Shared.Random;
using Robust.Shared.Timing; using Robust.Shared.Timing;
namespace Content.Server.Body.Systems namespace Content.Server.Body.Systems;
/// <inheritdoc/>
public sealed class MetabolizerSystem : SharedMetabolizerSystem
{ {
/// <inheritdoc/> [Dependency] private readonly IGameTiming _gameTiming = default!;
public sealed class MetabolizerSystem : SharedMetabolizerSystem [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<OrganComponent> _organQuery;
private EntityQuery<SolutionContainerManagerComponent> _solutionQuery;
private static readonly ProtoId<MetabolismGroupPrototype> Gas = "Gas";
public override void Initialize()
{ {
[Dependency] private readonly IGameTiming _gameTiming = default!; base.Initialize();
[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!;
private EntityQuery<OrganComponent> _organQuery; _organQuery = GetEntityQuery<OrganComponent>();
private EntityQuery<SolutionContainerManagerComponent> _solutionQuery; _solutionQuery = GetEntityQuery<SolutionContainerManagerComponent>();
public override void Initialize() SubscribeLocalEvent<MetabolizerComponent, ComponentInit>(OnMetabolizerInit);
SubscribeLocalEvent<MetabolizerComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<MetabolizerComponent, ApplyMetabolicMultiplierEvent>(OnApplyMetabolicMultiplier);
}
private void OnMapInit(Entity<MetabolizerComponent> ent, ref MapInitEvent args)
{
ent.Comp.NextUpdate = _gameTiming.CurTime + ent.Comp.AdjustedUpdateInterval;
}
private void OnMetabolizerInit(Entity<MetabolizerComponent> entity, ref ComponentInit args)
{
if (!entity.Comp.SolutionOnBody)
{ {
base.Initialize(); _solutionContainerSystem.EnsureSolution(entity.Owner, entity.Comp.SolutionName, out _);
_organQuery = GetEntityQuery<OrganComponent>();
_solutionQuery = GetEntityQuery<SolutionContainerManagerComponent>();
SubscribeLocalEvent<MetabolizerComponent, ComponentInit>(OnMetabolizerInit);
SubscribeLocalEvent<MetabolizerComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<MetabolizerComponent, EntityUnpausedEvent>(OnUnpaused);
SubscribeLocalEvent<MetabolizerComponent, ApplyMetabolicMultiplierEvent>(OnApplyMetabolicMultiplier);
} }
else if (_organQuery.CompOrNull(entity)?.Body is { } body)
private void OnMapInit(Entity<MetabolizerComponent> ent, ref MapInitEvent args)
{ {
ent.Comp.NextUpdate = _gameTiming.CurTime + ent.Comp.AdjustedUpdateInterval; _solutionContainerSystem.EnsureSolution(body, entity.Comp.SolutionName, out _);
}
private void OnUnpaused(Entity<MetabolizerComponent> ent, ref EntityUnpausedEvent args)
{
ent.Comp.NextUpdate += args.PausedTime;
}
private void OnMetabolizerInit(Entity<MetabolizerComponent> 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<MetabolizerComponent> 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<MetabolizerComponent>());
var query = EntityQueryEnumerator<MetabolizerComponent>();
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<MetabolizerComponent, OrganComponent?, SolutionContainerManagerComponent?> 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<SolutionComponent>? 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<ReagentPrototype>(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<MobStateComponent>(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);
} }
} }
private void OnApplyMetabolicMultiplier(Entity<MetabolizerComponent> 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<MetabolizerComponent>());
var query = EntityQueryEnumerator<MetabolizerComponent>();
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<MetabolizerComponent, OrganComponent?, SolutionContainerManagerComponent?> 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<SolutionComponent>? 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<ReagentPrototype>(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<MobStateComponent>(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);
}
/// <summary>
/// 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!
/// </summary>
/// <param name="body">The body metabolizing the effects</param>
/// <param name="organ">The organ doing the metabolizing</param>
/// <param name="solution">The solution we are metabolizing from</param>
/// <param name="conditions">The conditions that need to be met to metabolize</param>
/// <returns>True if we can metabolize! False if we cannot!</returns>
public bool CanMetabolizeEffect(EntityUid body, EntityUid organ, Entity<SolutionComponent> 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;
}
} }

View File

@@ -2,7 +2,6 @@ using Content.Server.Administration.Logs;
using Content.Server.Atmos.EntitySystems; using Content.Server.Atmos.EntitySystems;
using Content.Server.Body.Components; using Content.Server.Body.Components;
using Content.Server.Chat.Systems; using Content.Server.Chat.Systems;
using Content.Server.EntityEffects;
using Content.Shared.Body.Systems; using Content.Shared.Body.Systems;
using Content.Shared.Alert; using Content.Shared.Alert;
using Content.Shared.Atmos; using Content.Shared.Atmos;
@@ -14,9 +13,11 @@ using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reagent; using Content.Shared.Chemistry.Reagent;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.Database; using Content.Shared.Database;
using Content.Shared.EntityConditions;
using Content.Shared.EntityConditions.Conditions.Body;
using Content.Shared.EntityEffects; using Content.Shared.EntityEffects;
using Content.Shared.EntityEffects.EffectConditions;
using Content.Shared.EntityEffects.Effects; using Content.Shared.EntityEffects.Effects;
using Content.Shared.EntityEffects.Effects.Body;
using Content.Shared.Mobs.Systems; using Content.Shared.Mobs.Systems;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
@@ -29,16 +30,16 @@ public sealed class RespiratorSystem : EntitySystem
{ {
[Dependency] private readonly IAdminLogManager _adminLogger = default!; [Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IPrototypeManager _protoMan = default!;
[Dependency] private readonly AlertsSystem _alertsSystem = default!; [Dependency] private readonly AlertsSystem _alertsSystem = default!;
[Dependency] private readonly AtmosphereSystem _atmosSys = default!; [Dependency] private readonly AtmosphereSystem _atmosSys = default!;
[Dependency] private readonly BodySystem _bodySystem = default!; [Dependency] private readonly BodySystem _bodySystem = default!;
[Dependency] private readonly ChatSystem _chat = default!;
[Dependency] private readonly DamageableSystem _damageableSys = default!; [Dependency] private readonly DamageableSystem _damageableSys = default!;
[Dependency] private readonly LungSystem _lungSystem = default!; [Dependency] private readonly LungSystem _lungSystem = default!;
[Dependency] private readonly MobStateSystem _mobState = 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 SharedSolutionContainerSystem _solutionContainerSystem = default!;
[Dependency] private readonly ChatSystem _chat = default!;
[Dependency] private readonly EntityEffectSystem _entityEffect = default!;
private static readonly ProtoId<MetabolismGroupPrototype> GasId = new("Gas"); private static readonly ProtoId<MetabolismGroupPrototype> 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 // 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. // Applying actual reaction effects require a full ReagentEffectArgs struct.
bool CanMetabolize(EntityEffect effect) bool CanMetabolize(EntityEffect effect)
@@ -348,9 +348,10 @@ public sealed class RespiratorSystem : EntitySystem
if (effect.Conditions == null) if (effect.Conditions == null)
return true; return true;
// TODO: Use Metabolism Public API to do this instead, once that API has been built.
foreach (var cond in effect.Conditions) 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; return false;
} }

View File

@@ -1,7 +1,7 @@
using Content.Server.Body.Components; using Content.Server.Body.Components;
using Content.Server.Temperature.Components;
using Content.Server.Temperature.Systems; using Content.Server.Temperature.Systems;
using Content.Shared.ActionBlocker; using Content.Shared.ActionBlocker;
using Content.Shared.Temperature.Components;
using Robust.Shared.Timing; using Robust.Shared.Timing;
namespace Content.Server.Body.Systems; namespace Content.Server.Body.Systems;

View File

@@ -1,8 +1,9 @@
using Content.Server.Botany.Components; using Content.Server.Botany.Components;
using Content.Server.Botany.Systems; using Content.Server.Botany.Systems;
using Content.Server.EntityEffects; using Content.Server.EntityEffects.Effects.Botany;
using Content.Shared.Atmos; using Content.Shared.Atmos;
using Content.Shared.Database; using Content.Shared.Database;
using Content.Shared.EntityEffects;
using Content.Shared.Random; using Content.Shared.Random;
using Robust.Shared.Audio; using Robust.Shared.Audio;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
@@ -79,9 +80,13 @@ public partial struct SeedChemQuantity
[DataField("Inherent")] public bool Inherent = true; [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.
/// <remarks>
/// 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.
/// </remarks>
// TODO: Hit Botany with hammers
[Virtual, DataDefinition] [Virtual, DataDefinition]
[Access(typeof(BotanySystem), typeof(PlantHolderSystem), typeof(SeedExtractorSystem), typeof(PlantHolderComponent), typeof(EntityEffectSystem), typeof(MutationSystem))]
public partial class SeedData public partial class SeedData
{ {
#region Tracking #region Tracking

View File

@@ -7,6 +7,8 @@ namespace Content.Server.Botany.Systems;
public sealed partial class BotanySystem public sealed partial class BotanySystem
{ {
[Dependency] private readonly SharedEntityEffectsSystem _entityEffects = default!;
public void ProduceGrown(EntityUid uid, ProduceComponent produce) public void ProduceGrown(EntityUid uid, ProduceComponent produce)
{ {
if (!TryGetSeed(produce, out var seed)) if (!TryGetSeed(produce, out var seed))
@@ -15,10 +17,7 @@ public sealed partial class BotanySystem
foreach (var mutation in seed.Mutations) foreach (var mutation in seed.Mutations)
{ {
if (mutation.AppliesToProduce) if (mutation.AppliesToProduce)
{ _entityEffects.TryApplyEffect(uid, mutation.Effect);
var args = new EntityEffectBaseArgs(uid, EntityManager);
mutation.Effect.Effect(args);
}
} }
if (!_solutionContainerSystem.EnsureSolution(uid, if (!_solutionContainerSystem.EnsureSolution(uid,

View File

@@ -13,6 +13,7 @@ public sealed class MutationSystem : EntitySystem
[Dependency] private readonly IRobustRandom _robustRandom = default!; [Dependency] private readonly IRobustRandom _robustRandom = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly SharedEntityEffectsSystem _entityEffects = default!;
private RandomPlantMutationListPrototype _randomMutations = default!; private RandomPlantMutationListPrototype _randomMutations = default!;
public override void Initialize() public override void Initialize()
@@ -32,10 +33,8 @@ public sealed class MutationSystem : EntitySystem
if (Random(Math.Min(mutation.BaseOdds * severity, 1.0f))) if (Random(Math.Min(mutation.BaseOdds * severity, 1.0f)))
{ {
if (mutation.AppliesToPlant) if (mutation.AppliesToPlant)
{ _entityEffects.TryApplyEffect(plantHolder, mutation.Effect);
var args = new EntityEffectBaseArgs(plantHolder, EntityManager);
mutation.Effect.Effect(args);
}
// Stat adjustments do not persist by being an attached effect, they just change the stat. // 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)) if (mutation.Persists && !seed.Mutations.Any(m => m.Name == mutation.Name))
seed.Mutations.Add(mutation); seed.Mutations.Add(mutation);

View File

@@ -24,8 +24,10 @@ using Robust.Shared.Prototypes;
using Robust.Shared.Random; using Robust.Shared.Random;
using Robust.Shared.Timing; using Robust.Shared.Timing;
using Content.Shared.Administration.Logs; using Content.Shared.Administration.Logs;
using Content.Shared.Chemistry.Reaction;
using Content.Shared.Containers.ItemSlots; using Content.Shared.Containers.ItemSlots;
using Content.Shared.Database; using Content.Shared.Database;
using Content.Shared.EntityEffects;
using Content.Shared.Kitchen.Components; using Content.Shared.Kitchen.Components;
using Content.Shared.Labels.Components; using Content.Shared.Labels.Components;
@@ -48,6 +50,7 @@ public sealed class PlantHolderSystem : EntitySystem
[Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly ItemSlotsSystem _itemSlots = default!; [Dependency] private readonly ItemSlotsSystem _itemSlots = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly SharedEntityEffectsSystem _entityEffects = default!;
public const float HydroponicsSpeedMultiplier = 1f; public const float HydroponicsSpeedMultiplier = 1f;
public const float HydroponicsConsumptionMultiplier = 2f; public const float HydroponicsConsumptionMultiplier = 2f;
@@ -887,7 +890,7 @@ public sealed class PlantHolderSystem : EntitySystem
foreach (var entry in _solutionContainerSystem.RemoveEachReagent(component.SoilSolution.Value, amt)) foreach (var entry in _solutionContainerSystem.RemoveEachReagent(component.SoilSolution.Value, amt))
{ {
var reagentProto = _prototype.Index<ReagentPrototype>(entry.Reagent.Prototype); var reagentProto = _prototype.Index<ReagentPrototype>(entry.Reagent.Prototype);
reagentProto.ReactionPlant(uid, entry, solution, EntityManager, _random, _adminLogger); _entityEffects.ApplyEffects(uid, reagentProto.PlantMetabolisms.ToArray());
} }
} }

View File

@@ -39,7 +39,7 @@ public sealed class DumpReagentGuideText : LocalizedEntityCommands
{ {
foreach (var effect in entry.Effects) 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()))); Loc.GetString($"cmd-dumpreagentguidetext-skipped", ("effect", effect.GetType())));
} }
} }

View File

@@ -13,6 +13,7 @@ using Content.Shared.Prying.Systems;
using Content.Shared.Radio.EntitySystems; using Content.Shared.Radio.EntitySystems;
using Content.Shared.Stacks; using Content.Shared.Stacks;
using Content.Shared.Temperature; using Content.Shared.Temperature;
using Content.Shared.Temperature.Components;
using Content.Shared.Tools.Systems; using Content.Shared.Tools.Systems;
using Robust.Shared.Containers; using Robust.Shared.Containers;
using Robust.Shared.Utility; using Robust.Shared.Utility;

View File

@@ -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;
/// <summary>
/// Returns true if this entity is both able to breathe and is currently breathing.
/// </summary>
/// <inheritdoc cref="EntityConditionSystem{T, TCondition}"/>
public sealed partial class IsBreathingEntityConditionSystem : EntityConditionSystem<RespiratorComponent, BreathingCondition>
{
[Dependency] private readonly RespiratorSystem _respirator = default!;
protected override void Condition(Entity<RespiratorComponent> entity, ref EntityConditionEvent<BreathingCondition> args)
{
args.Result = _respirator.IsBreathing(entity.AsNullable());
}
}

View File

@@ -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;
/// <summary>
/// Returns true if this entity has any of the listed metabolizer types.
/// </summary>
/// <inheritdoc cref="EntityConditionSystem{T, TCondition}"/>
public sealed partial class MetabolizerTypeEntityConditionSystem : EntityConditionSystem<MetabolizerComponent, MetabolizerTypeCondition>
{
protected override void Condition(Entity<MetabolizerComponent> entity, ref EntityConditionEvent<MetabolizerTypeCondition> args)
{
if (entity.Comp.MetabolizerTypes == null)
return;
args.Result = entity.Comp.MetabolizerTypes.Overlaps(args.Condition.Type);
}
}

View File

@@ -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;
/// <summary>
/// This effect adjusts a gas at the tile this entity is currently on.
/// The amount changed is modified by scale.
/// </summary>
/// <inheritdoc cref="EntityEffectSystem{T,TEffect}"/>
public sealed partial class CreateGasEntityEffectSystem : EntityEffectSystem<TransformComponent, CreateGas>
{
[Dependency] private readonly AtmosphereSystem _atmosphere = default!;
protected override void Effect(Entity<TransformComponent> entity, ref EntityEffectEvent<CreateGas> args)
{
var tileMix = _atmosphere.GetContainingMixture(entity.AsNullable(), false, true);
tileMix?.AdjustMoles(args.Effect.Gas, args.Scale * args.Effect.Moles);
}
}

View File

@@ -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;
/// <summary>
/// Adds a number of FireStacks modified by scale to this entity.
/// The amount of FireStacks added is modified by scale.
/// </summary>
/// <inheritdoc cref="EntityEffectSystem{T,TEffect}"/>
public sealed partial class FlammableEntityEffectSystem : EntityEffectSystem<FlammableComponent, Flammable>
{
[Dependency] private readonly FlammableSystem _flammable = default!;
protected override void Effect(Entity<FlammableComponent> entity, ref EntityEffectEvent<Flammable> 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);
}
}

View File

@@ -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;
/// <summary>
/// Sets this entity on fire.
/// </summary>
/// <inheritdoc cref="EntityEffectSystem{T,TEffect}"/>
public sealed partial class IngiteEntityEffectSystem : EntityEffectSystem<FlammableComponent, Ignite>
{
[Dependency] private readonly FlammableSystem _flammable = default!;
protected override void Effect(Entity<FlammableComponent> entity, ref EntityEffectEvent<Ignite> 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);
}
}

View File

@@ -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;
/// <summary>
/// This effect adjusts a respirator's saturation value.
/// The saturation adjustment is modified by scale.
/// </summary>
/// <inheritdoc cref="EntityEffectSystem{T,TEffect}"/>
public sealed partial class OxygenateEntityEffectsSystem : EntityEffectSystem<RespiratorComponent, Oxygenate>
{
[Dependency] private readonly RespiratorSystem _respirator = default!;
protected override void Effect(Entity<RespiratorComponent> entity, ref EntityEffectEvent<Oxygenate> args)
{
_respirator.UpdateSaturation(entity, args.Scale * args.Effect.Factor, entity.Comp);
}
}

View File

@@ -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<PlantHolderComponent, PlantAdjustHealth>
{
[Dependency] private readonly PlantHolderSystem _plantHolder = default!;
protected override void Effect(Entity<PlantHolderComponent> entity, ref EntityEffectEvent<PlantAdjustHealth> args)
{
if (entity.Comp.Seed == null || entity.Comp.Dead)
return;
entity.Comp.MutationLevel += args.Effect.Amount * entity.Comp.MutationMod;
_plantHolder.CheckHealth(entity, entity.Comp);
}
}

View File

@@ -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<PlantHolderComponent, PlantAdjustMutationLevel>
{
[Dependency] private readonly PlantHolderSystem _plantHolder = default!;
protected override void Effect(Entity<PlantHolderComponent> entity, ref EntityEffectEvent<PlantAdjustMutationLevel> args)
{
if (entity.Comp.Seed == null || entity.Comp.Dead)
return;
entity.Comp.Health += args.Effect.Amount;
_plantHolder.CheckHealth(entity, entity.Comp);
}
}

View File

@@ -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<PlantHolderComponent, PlantAdjustMutationMod>
{
protected override void Effect(Entity<PlantHolderComponent> entity, ref EntityEffectEvent<PlantAdjustMutationMod> args)
{
if (entity.Comp.Seed == null || entity.Comp.Dead)
return;
entity.Comp.MutationMod += args.Effect.Amount;
}
}

View File

@@ -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<PlantHolderComponent, PlantAdjustNutrition>
{
[Dependency] private readonly PlantHolderSystem _plantHolder = default!;
protected override void Effect(Entity<PlantHolderComponent> entity, ref EntityEffectEvent<PlantAdjustNutrition> args)
{
_plantHolder.AdjustNutrient(entity, args.Effect.Amount, entity);
}
}

View File

@@ -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<PlantHolderComponent, PlantAdjustPests>
{
protected override void Effect(Entity<PlantHolderComponent> entity, ref EntityEffectEvent<PlantAdjustPests> args)
{
if (entity.Comp.Seed == null || entity.Comp.Dead)
return;
entity.Comp.PestLevel += args.Effect.Amount;
}
}

View File

@@ -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<PlantHolderComponent, PlantAdjustPotency>
{
[Dependency] private readonly PlantHolderSystem _plantHolder = default!;
protected override void Effect(Entity<PlantHolderComponent> entity, ref EntityEffectEvent<PlantAdjustPotency> 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);
}
}

View File

@@ -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<PlantHolderComponent, PlantAdjustToxins>
{
protected override void Effect(Entity<PlantHolderComponent> entity, ref EntityEffectEvent<PlantAdjustToxins> args)
{
if (entity.Comp.Seed == null || entity.Comp.Dead)
return;
entity.Comp.Toxins += args.Effect.Amount;
}
}

View File

@@ -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<PlantHolderComponent, PlantAdjustWater>
{
[Dependency] private readonly PlantHolderSystem _plantHolder = default!;
protected override void Effect(Entity<PlantHolderComponent> entity, ref EntityEffectEvent<PlantAdjustWater> args)
{
_plantHolder.AdjustWater(entity, args.Effect.Amount, entity.Comp);
}
}

View File

@@ -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<PlantHolderComponent, PlantAdjustWeeds>
{
protected override void Effect(Entity<PlantHolderComponent> entity, ref EntityEffectEvent<PlantAdjustWeeds> args)
{
if (entity.Comp.Seed == null || entity.Comp.Dead)
return;
entity.Comp.WeedLevel += args.Effect.Amount;
}
}

View File

@@ -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<PlantHolderComponent, PlantAffectGrowth>
{
[Dependency] private readonly PlantHolderSystem _plantHolder = default!;
protected override void Effect(Entity<PlantHolderComponent> entity, ref EntityEffectEvent<PlantAffectGrowth> args)
{
if (entity.Comp.Seed == null || entity.Comp.Dead)
return;
_plantHolder.AffectGrowth(entity, (int)args.Effect.Amount, entity);
}
}

View File

@@ -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;
/// <summary>
/// This system mutates an inputted stat for a PlantHolder, only works for floats, integers, and bools.
/// </summary>
/// <inheritdoc cref="EntityEffectSystem{T,TEffect}"/>
public sealed partial class PlantChangeStatEntityEffectSystem : EntityEffectSystem<PlantHolderComponent, PlantChangeStat>
{
// 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<PlantHolderComponent> entity, ref EntityEffectEvent<PlantChangeStat> 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;
}
}

View File

@@ -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<PlantHolderComponent, PlantCryoxadone>
{
[Dependency] private readonly IRobustRandom _random = default!;
protected override void Effect(Entity<PlantHolderComponent> entity, ref EntityEffectEvent<PlantCryoxadone> 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;
}
}

View File

@@ -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<PlantHolderComponent, PlantDestroySeeds>
{
[Dependency] private readonly PlantHolderSystem _plantHolder = default!;
[Dependency] private readonly PopupSystem _popup = default!;
protected override void Effect(Entity<PlantHolderComponent> entity, ref EntityEffectEvent<PlantDestroySeeds> 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;
}
}

View File

@@ -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<PlantHolderComponent, PlantDiethylamine>
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly PlantHolderSystem _plantHolder = default!;
protected override void Effect(Entity<PlantHolderComponent> entity, ref EntityEffectEvent<PlantDiethylamine> 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++;
}
}
}

View File

@@ -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<PlantHolderComponent, PlantPhalanximine>
{
protected override void Effect(Entity<PlantHolderComponent> entity, ref EntityEffectEvent<PlantPhalanximine> args)
{
if (entity.Comp.Seed == null || entity.Comp.Dead || entity.Comp.Seed.Immutable)
return;
entity.Comp.Seed.Viable = true;
}
}

View File

@@ -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<PlantHolderComponent, PlantRestoreSeeds>
{
[Dependency] private readonly PlantHolderSystem _plantHolder = default!;
[Dependency] private readonly PopupSystem _popup = default!;
protected override void Effect(Entity<PlantHolderComponent> entity, ref EntityEffectEvent<PlantRestoreSeeds> 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;
}
}

View File

@@ -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;
/// <summary>
/// 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.
/// </summary>
/// <inheritdoc cref="EntityEffectSystem{T,TEffect}"/>
public sealed partial class RobustHarvestEntityEffectSystem : EntityEffectSystem<PlantHolderComponent, RobustHarvest>
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly PlantHolderSystem _plantHolder = default!;
protected override void Effect(Entity<PlantHolderComponent> entity, ref EntityEffectEvent<RobustHarvest> 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--;
}
}
}

View File

@@ -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<PlantHolderComponent, PlantMutateChemicals>
{
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IRobustRandom _random = default!;
protected override void Effect(Entity<PlantHolderComponent> entity, ref EntityEffectEvent<PlantMutateChemicals> 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;
}
}

View File

@@ -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<PlantHolderComponent, PlantMutateExudeGases>
{
[Dependency] private readonly IRobustRandom _random = default!;
protected override void Effect(Entity<PlantHolderComponent> entity, ref EntityEffectEvent<PlantMutateExudeGases> 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<Gas>().ToList());
if (!gasses.TryAdd(gas, amount))
{
gasses[gas] += amount;
}
}
}
public sealed partial class PlantMutateConsumeGasesEntityEffectSystem : EntityEffectSystem<PlantHolderComponent, PlantMutateConsumeGases>
{
[Dependency] private readonly IRobustRandom _random = default!;
protected override void Effect(Entity<PlantHolderComponent> entity, ref EntityEffectEvent<PlantMutateConsumeGases> 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<Gas>().ToList());
if (!gasses.TryAdd(gas, amount))
{
gasses[gas] += amount;
}
}
}

View File

@@ -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<PlantHolderComponent, PlantMutateHarvest>
{
protected override void Effect(Entity<PlantHolderComponent> entity, ref EntityEffectEvent<PlantMutateHarvest> 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;
}
}
}

View File

@@ -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<PlantHolderComponent, PlantMutateSpeciesChange>
{
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IRobustRandom _random = default!;
protected override void Effect(Entity<PlantHolderComponent> entity, ref EntityEffectEvent<PlantMutateSpeciesChange> 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);
}
}

View File

@@ -0,0 +1,22 @@
using Content.Server.Chat.Systems;
using Content.Shared.EntityEffects;
using Content.Shared.EntityEffects.Effects;
namespace Content.Server.EntityEffects.Effects;
/// <summary>
/// Makes this entity emote.
/// </summary>
/// <inheritdoc cref="EntityEffectSystem{T,TEffect}"/>
public sealed partial class EmoteEntityEffectSystem : EntityEffectSystem<MetaDataComponent, Emote>
{
[Dependency] private readonly ChatSystem _chat = default!;
protected override void Effect(Entity<MetaDataComponent> entity, ref EntityEffectEvent<Emote> args)
{
if (args.Effect.ShowInChat)
_chat.TryEmoteWithChat(entity, args.Effect.EmoteId, ChatTransmitRange.GhostRangeLimit, forceEmote: args.Effect.Force);
else
_chat.TryEmoteWithoutChat(entity, args.Effect.EmoteId);
}
}

View File

@@ -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;
/// <summary>
/// Makes this entity sentient. Allows ghost to take it over if it's not already occupied.
/// Optionally also allows this entity to speak.
/// </summary>
/// <inheritdoc cref="EntityEffectSystem{T,TEffect}"/>
public sealed partial class MakeSentientEntityEffectSystem : EntityEffectSystem<MetaDataComponent, MakeSentient>
{
protected override void Effect(Entity<MetaDataComponent> entity, ref EntityEffectEvent<MakeSentient> 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<ReplacementAccentComponent>(entity);
// TODO: Make MonkeyAccent a replacement accent and remove MonkeyAccent code-smell.
RemComp<MonkeyAccentComponent>(entity);
}
// Stops from adding a ghost role to things like people who already have a mind
if (TryComp<MindContainerComponent>(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<GhostRoleComponent>(entity);
EnsureComp<GhostTakeoverAvailableComponent>(entity);
ghostRole.RoleName = entity.Comp.EntityName;
ghostRole.RoleDescription = Loc.GetString("ghost-role-information-cognizine-description");
}
}

View File

@@ -0,0 +1,19 @@
using Content.Server.Polymorph.Components;
using Content.Server.Polymorph.Systems;
using Content.Shared.EntityEffects;
namespace Content.Server.EntityEffects.Effects;
/// <summary>
/// Polymorphs this entity into another entity.
/// </summary>
/// <inheritdoc cref="EntityEffectSystem{T,TEffect}"/>
public sealed partial class PolymorphEntityEffectSystem : EntityEffectSystem<PolymorphableComponent, Shared.EntityEffects.Effects.Polymorph>
{
[Dependency] private readonly PolymorphSystem _polymorph = default!;
protected override void Effect(Entity<PolymorphableComponent> entity, ref EntityEffectEvent<Shared.EntityEffects.Effects.Polymorph> args)
{
_polymorph.PolymorphEntity(entity, args.Effect.Prototype);
}
}

View File

@@ -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;
/// <summary>
/// This effect creates smoke at this solution's position.
/// The amount of smoke created is modified by scale.
/// </summary>
/// <inheritdoc cref="EntityEffectSystem{T,TEffect}"/>
public sealed partial class AreaReactionEntityEffectsSystem : EntityEffectSystem<SolutionComponent, AreaReactionEffect>
{
[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<SolutionComponent> entity, ref EntityEffectEvent<AreaReactionEffect> 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));
}
}

View File

@@ -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;
/// <summary>
/// Creates an explosion at this entity's position.
/// Intensity is modified by scale.
/// </summary>
/// <inheritdoc cref="EntityEffectSystem{T,TEffect}"/>
public sealed partial class ExplosionEntityEffectSystem : EntityEffectSystem<TransformComponent, ExplosionEffect>
{
[Dependency] private readonly ExplosionSystem _explosion = default!;
protected override void Effect(Entity<TransformComponent> entity, ref EntityEffectEvent<ExplosionEffect> 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);
}
}

View File

@@ -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<WeightedRandomFillSolutionPrototype> 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<CheckEntityEffectConditionEvent<TemperatureCondition>>(OnCheckTemperature);
SubscribeLocalEvent<CheckEntityEffectConditionEvent<Breathing>>(OnCheckBreathing);
SubscribeLocalEvent<CheckEntityEffectConditionEvent<OrganType>>(OnCheckOrganType);
SubscribeLocalEvent<ExecuteEntityEffectEvent<PlantAdjustHealth>>(OnExecutePlantAdjustHealth);
SubscribeLocalEvent<ExecuteEntityEffectEvent<PlantAdjustMutationLevel>>(OnExecutePlantAdjustMutationLevel);
SubscribeLocalEvent<ExecuteEntityEffectEvent<PlantAdjustMutationMod>>(OnExecutePlantAdjustMutationMod);
SubscribeLocalEvent<ExecuteEntityEffectEvent<PlantAdjustNutrition>>(OnExecutePlantAdjustNutrition);
SubscribeLocalEvent<ExecuteEntityEffectEvent<PlantAdjustPests>>(OnExecutePlantAdjustPests);
SubscribeLocalEvent<ExecuteEntityEffectEvent<PlantAdjustPotency>>(OnExecutePlantAdjustPotency);
SubscribeLocalEvent<ExecuteEntityEffectEvent<PlantAdjustToxins>>(OnExecutePlantAdjustToxins);
SubscribeLocalEvent<ExecuteEntityEffectEvent<PlantAdjustWater>>(OnExecutePlantAdjustWater);
SubscribeLocalEvent<ExecuteEntityEffectEvent<PlantAdjustWeeds>>(OnExecutePlantAdjustWeeds);
SubscribeLocalEvent<ExecuteEntityEffectEvent<PlantAffectGrowth>>(OnExecutePlantAffectGrowth);
SubscribeLocalEvent<ExecuteEntityEffectEvent<PlantChangeStat>>(OnExecutePlantChangeStat);
SubscribeLocalEvent<ExecuteEntityEffectEvent<PlantCryoxadone>>(OnExecutePlantCryoxadone);
SubscribeLocalEvent<ExecuteEntityEffectEvent<PlantDestroySeeds>>(OnExecutePlantDestroySeeds);
SubscribeLocalEvent<ExecuteEntityEffectEvent<PlantDiethylamine>>(OnExecutePlantDiethylamine);
SubscribeLocalEvent<ExecuteEntityEffectEvent<PlantPhalanximine>>(OnExecutePlantPhalanximine);
SubscribeLocalEvent<ExecuteEntityEffectEvent<PlantRestoreSeeds>>(OnExecutePlantRestoreSeeds);
SubscribeLocalEvent<ExecuteEntityEffectEvent<RobustHarvest>>(OnExecuteRobustHarvest);
SubscribeLocalEvent<ExecuteEntityEffectEvent<AdjustTemperature>>(OnExecuteAdjustTemperature);
SubscribeLocalEvent<ExecuteEntityEffectEvent<AreaReactionEffect>>(OnExecuteAreaReactionEffect);
SubscribeLocalEvent<ExecuteEntityEffectEvent<CauseZombieInfection>>(OnExecuteCauseZombieInfection);
SubscribeLocalEvent<ExecuteEntityEffectEvent<ChemCleanBloodstream>>(OnExecuteChemCleanBloodstream);
SubscribeLocalEvent<ExecuteEntityEffectEvent<ChemVomit>>(OnExecuteChemVomit);
SubscribeLocalEvent<ExecuteEntityEffectEvent<CreateEntityReactionEffect>>(OnExecuteCreateEntityReactionEffect);
SubscribeLocalEvent<ExecuteEntityEffectEvent<CreateGas>>(OnExecuteCreateGas);
SubscribeLocalEvent<ExecuteEntityEffectEvent<CureZombieInfection>>(OnExecuteCureZombieInfection);
SubscribeLocalEvent<ExecuteEntityEffectEvent<Emote>>(OnExecuteEmote);
SubscribeLocalEvent<ExecuteEntityEffectEvent<EmpReactionEffect>>(OnExecuteEmpReactionEffect);
SubscribeLocalEvent<ExecuteEntityEffectEvent<ExplosionReactionEffect>>(OnExecuteExplosionReactionEffect);
SubscribeLocalEvent<ExecuteEntityEffectEvent<FlammableReaction>>(OnExecuteFlammableReaction);
SubscribeLocalEvent<ExecuteEntityEffectEvent<FlashReactionEffect>>(OnExecuteFlashReactionEffect);
SubscribeLocalEvent<ExecuteEntityEffectEvent<Ignite>>(OnExecuteIgnite);
SubscribeLocalEvent<ExecuteEntityEffectEvent<MakeSentient>>(OnExecuteMakeSentient);
SubscribeLocalEvent<ExecuteEntityEffectEvent<ModifyBleedAmount>>(OnExecuteModifyBleedAmount);
SubscribeLocalEvent<ExecuteEntityEffectEvent<ModifyBloodLevel>>(OnExecuteModifyBloodLevel);
SubscribeLocalEvent<ExecuteEntityEffectEvent<ModifyLungGas>>(OnExecuteModifyLungGas);
SubscribeLocalEvent<ExecuteEntityEffectEvent<Oxygenate>>(OnExecuteOxygenate);
SubscribeLocalEvent<ExecuteEntityEffectEvent<PlantMutateChemicals>>(OnExecutePlantMutateChemicals);
SubscribeLocalEvent<ExecuteEntityEffectEvent<PlantMutateConsumeGasses>>(OnExecutePlantMutateConsumeGasses);
SubscribeLocalEvent<ExecuteEntityEffectEvent<PlantMutateExudeGasses>>(OnExecutePlantMutateExudeGasses);
SubscribeLocalEvent<ExecuteEntityEffectEvent<PlantMutateHarvest>>(OnExecutePlantMutateHarvest);
SubscribeLocalEvent<ExecuteEntityEffectEvent<PlantSpeciesChange>>(OnExecutePlantSpeciesChange);
SubscribeLocalEvent<ExecuteEntityEffectEvent<PolymorphEffect>>(OnExecutePolymorph);
SubscribeLocalEvent<ExecuteEntityEffectEvent<ResetNarcolepsy>>(OnExecuteResetNarcolepsy);
}
private void OnCheckTemperature(ref CheckEntityEffectConditionEvent<TemperatureCondition> 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<Breathing> 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<OrganType> 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<MetabolizerComponent?> metabolizer)
{
metabolizer.Comp ??= EntityManager.GetComponentOrNull<MetabolizerComponent>(metabolizer.Owner);
if (metabolizer.Comp != null
&& metabolizer.Comp.MetabolizerTypes != null
&& metabolizer.Comp.MetabolizerTypes.Contains(condition.Type))
return condition.ShouldHave;
return !condition.ShouldHave;
}
/// <summary>
/// Checks if the plant holder can metabolize the reagent or not. Checks if it has an alive plant by default.
/// </summary>
/// <param name="plantHolder">The entity holding the plant</param>
/// <param name="plantHolderComponent">The plant holder component</param>
/// <param name="entityManager">The entity manager</param>
/// <param name="mustHaveAlivePlant">Whether to check if it has an alive plant or not</param>
/// <returns></returns>
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<PlantAdjustHealth> 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<PlantAdjustMutationLevel> args)
{
if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp))
return;
plantHolderComp.MutationLevel += args.Effect.Amount * plantHolderComp.MutationMod;
}
private void OnExecutePlantAdjustMutationMod(ref ExecuteEntityEffectEvent<PlantAdjustMutationMod> args)
{
if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp))
return;
plantHolderComp.MutationMod += args.Effect.Amount;
}
private void OnExecutePlantAdjustNutrition(ref ExecuteEntityEffectEvent<PlantAdjustNutrition> 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<PlantAdjustPests> args)
{
if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp))
return;
plantHolderComp.PestLevel += args.Effect.Amount;
}
private void OnExecutePlantAdjustPotency(ref ExecuteEntityEffectEvent<PlantAdjustPotency> 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<PlantAdjustToxins> args)
{
if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp))
return;
plantHolderComp.Toxins += args.Effect.Amount;
}
private void OnExecutePlantAdjustWater(ref ExecuteEntityEffectEvent<PlantAdjustWater> 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<PlantAdjustWeeds> args)
{
if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp))
return;
plantHolderComp.WeedLevel += args.Effect.Amount;
}
private void OnExecutePlantAffectGrowth(ref ExecuteEntityEffectEvent<PlantAffectGrowth> 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<PlantChangeStat> 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<PlantCryoxadone> 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<PlantDestroySeeds> 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<PlantDiethylamine> 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<PlantPhalanximine> args)
{
if (!CanMetabolizePlant(args.Args.TargetEntity, out var plantHolderComp, mustHaveMutableSeed: true))
return;
plantHolderComp.Seed!.Viable = true;
}
private void OnExecutePlantRestoreSeeds(ref ExecuteEntityEffectEvent<PlantRestoreSeeds> 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<RobustHarvest> 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<AdjustTemperature> 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<AreaReactionEffect> 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<TransformComponent>(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<CauseZombieInfection> args)
{
EnsureComp<ZombifyOnDeathComponent>(args.Args.TargetEntity);
EnsureComp<PendingZombieComponent>(args.Args.TargetEntity);
}
private void OnExecuteChemCleanBloodstream(ref ExecuteEntityEffectEvent<ChemCleanBloodstream> 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<ChemVomit> 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<CreateEntityReactionEffect> args)
{
var transform = Comp<TransformComponent>(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<CreateGas> 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<CureZombieInfection> args)
{
if (HasComp<IncurableZombieComponent>(args.Args.TargetEntity))
return;
RemComp<ZombifyOnDeathComponent>(args.Args.TargetEntity);
RemComp<PendingZombieComponent>(args.Args.TargetEntity);
if (args.Effect.Innoculate)
{
EnsureComp<ZombieImmuneComponent>(args.Args.TargetEntity);
}
}
private void OnExecuteEmote(ref ExecuteEntityEffectEvent<Emote> 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<EmpReactionEffect> args)
{
var transform = Comp<TransformComponent>(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<ExplosionReactionEffect> 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<FlammableReaction> 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<FlashReactionEffect> args)
{
var transform = Comp<TransformComponent>(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<PointLightComponent>(uid, out var pointLightComp))
return;
_pointLight.SetRadius(uid, MathF.Max(1.1f, range), pointLightComp);
}
private void OnExecuteIgnite(ref ExecuteEntityEffectEvent<Ignite> 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<MakeSentient> 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<ReplacementAccentComponent>(uid);
RemComp<MonkeyAccentComponent>(uid);
// Stops from adding a ghost role to things like people who already have a mind
if (TryComp<MindContainerComponent>(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<GhostRoleComponent>(uid);
EnsureComp<GhostTakeoverAvailableComponent>(uid);
var entityData = Comp<MetaDataComponent>(uid);
ghostRole.RoleName = entityData.EntityName;
ghostRole.RoleDescription = Loc.GetString("ghost-role-information-cognizine-description");
}
private void OnExecuteModifyBleedAmount(ref ExecuteEntityEffectEvent<ModifyBleedAmount> args)
{
if (TryComp<BloodstreamComponent>(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<ModifyBloodLevel> args)
{
if (TryComp<BloodstreamComponent>(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<ModifyLungGas> args)
{
LungComponent? lung;
float amount = 1f;
if (args.Args is EntityEffectReagentArgs reagentArgs)
{
if (!TryComp<LungComponent>(reagentArgs.OrganEntity, out var organLung))
return;
lung = organLung;
amount = reagentArgs.Quantity.Float();
}
else
{
if (!TryComp<LungComponent>(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<Oxygenate> args)
{
var multiplier = 1f;
if (args.Args is EntityEffectReagentArgs reagentArgs)
{
multiplier = reagentArgs.Quantity.Float();
}
if (TryComp<RespiratorComponent>(args.Args.TargetEntity, out var resp))
{
_respirator.UpdateSaturation(args.Args.TargetEntity, multiplier * args.Effect.Factor, resp);
}
}
private void OnExecutePlantMutateChemicals(ref ExecuteEntityEffectEvent<PlantMutateChemicals> args)
{
var plantholder = Comp<PlantHolderComponent>(args.Args.TargetEntity);
if (plantholder.Seed == null)
return;
var chemicals = plantholder.Seed.Chemicals;
var randomChems = _protoManager.Index(RandomPickBotanyReagent).Fills;
// Add a random amount of a random chemical to this set of chemicals
if (randomChems != null)
{
var pick = _random.Pick<RandomFillSolution>(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;
}
}
private void OnExecutePlantMutateConsumeGasses(ref ExecuteEntityEffectEvent<PlantMutateConsumeGasses> args)
{
var plantholder = Comp<PlantHolderComponent>(args.Args.TargetEntity);
if (plantholder.Seed == null)
return;
var gasses = plantholder.Seed.ConsumeGasses;
// Add a random amount of a random gas to this gas dictionary
float amount = _random.NextFloat(args.Effect.MinValue, args.Effect.MaxValue);
Gas gas = _random.Pick(Enum.GetValues(typeof(Gas)).Cast<Gas>().ToList());
if (gasses.ContainsKey(gas))
{
gasses[gas] += amount;
}
else
{
gasses.Add(gas, amount);
}
}
private void OnExecutePlantMutateExudeGasses(ref ExecuteEntityEffectEvent<PlantMutateExudeGasses> args)
{
var plantholder = Comp<PlantHolderComponent>(args.Args.TargetEntity);
if (plantholder.Seed == null)
return;
var gasses = plantholder.Seed.ExudeGasses;
// Add a random amount of a random gas to this gas dictionary
float amount = _random.NextFloat(args.Effect.MinValue, args.Effect.MaxValue);
Gas gas = _random.Pick(Enum.GetValues(typeof(Gas)).Cast<Gas>().ToList());
if (gasses.ContainsKey(gas))
{
gasses[gas] += amount;
}
else
{
gasses.Add(gas, amount);
}
}
private void OnExecutePlantMutateHarvest(ref ExecuteEntityEffectEvent<PlantMutateHarvest> args)
{
var plantholder = Comp<PlantHolderComponent>(args.Args.TargetEntity);
if (plantholder.Seed == null)
return;
if (plantholder.Seed.HarvestRepeat == HarvestType.NoRepeat)
plantholder.Seed.HarvestRepeat = HarvestType.Repeat;
else if (plantholder.Seed.HarvestRepeat == HarvestType.Repeat)
plantholder.Seed.HarvestRepeat = HarvestType.SelfHarvest;
}
private void OnExecutePlantSpeciesChange(ref ExecuteEntityEffectEvent<PlantSpeciesChange> args)
{
var plantholder = Comp<PlantHolderComponent>(args.Args.TargetEntity);
if (plantholder.Seed == null)
return;
if (plantholder.Seed.MutationPrototypes.Count == 0)
return;
var targetProto = _random.Pick(plantholder.Seed.MutationPrototypes);
if (!_protoManager.TryIndex(targetProto, out SeedPrototype? protoSeed))
{
Log.Error($"Seed prototype could not be found: {targetProto}!");
return;
}
plantholder.Seed = plantholder.Seed.SpeciesChange(protoSeed);
}
private void OnExecutePolymorph(ref ExecuteEntityEffectEvent<PolymorphEffect> args)
{
// Make it into a prototype
EnsureComp<PolymorphableComponent>(args.Args.TargetEntity);
_polymorph.PolymorphEntity(args.Args.TargetEntity, args.Effect.PolymorphPrototype);
}
private void OnExecuteResetNarcolepsy(ref ExecuteEntityEffectEvent<ResetNarcolepsy> args)
{
if (args.Args is EntityEffectReagentArgs reagentArgs)
if (reagentArgs.Scale != 1f)
return;
_narcolepsy.AdjustNarcolepsyTimer(args.Args.TargetEntity, args.Effect.TimerReset);
}
}

View File

@@ -21,7 +21,7 @@ using Robust.Shared.Prototypes;
using Robust.Shared.Random; using Robust.Shared.Random;
using Robust.Shared.Timing; using Robust.Shared.Timing;
using System.Linq; using System.Linq;
using Content.Shared.EntityEffects.Effects.Solution;
using TimedDespawnComponent = Robust.Shared.Spawners.TimedDespawnComponent; using TimedDespawnComponent = Robust.Shared.Spawners.TimedDespawnComponent;
namespace Content.Server.Fluids.EntitySystems; namespace Content.Server.Fluids.EntitySystems;
@@ -278,11 +278,10 @@ public sealed class SmokeSystem : EntitySystem
{ {
if (reagentQuantity.Quantity == FixedPoint2.Zero) if (reagentQuantity.Quantity == FixedPoint2.Zero)
continue; continue;
var reagentProto = _prototype.Index<ReagentPrototype>(reagentQuantity.Reagent.Prototype);
_reactive.ReactionEntity(entity, ReactionMethod.Touch, reagentProto, reagentQuantity, transferSolution); _reactive.ReactionEntity(entity, ReactionMethod.Touch, reagentQuantity);
if (!blockIngestion) if (!blockIngestion)
_reactive.ReactionEntity(entity, ReactionMethod.Ingestion, reagentProto, reagentQuantity, transferSolution); _reactive.ReactionEntity(entity, ReactionMethod.Ingestion, reagentQuantity);
} }
if (blockIngestion) if (blockIngestion)

View File

@@ -5,6 +5,7 @@ using System.Text.Json.Serialization;
using Content.Shared.Chemistry.Reaction; using Content.Shared.Chemistry.Reaction;
using Content.Shared.Chemistry.Reagent; using Content.Shared.Chemistry.Reagent;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.EntityConditions;
using Content.Shared.EntityEffects; using Content.Shared.EntityEffects;
using Content.Shared.FixedPoint; using Content.Shared.FixedPoint;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
@@ -42,7 +43,7 @@ public sealed class ChemistryJsonGenerator
Converters = Converters =
{ {
new UniversalJsonConverter<EntityEffect>(), new UniversalJsonConverter<EntityEffect>(),
new UniversalJsonConverter<EntityEffectCondition>(), new UniversalJsonConverter<EntityCondition>(),
new UniversalJsonConverter<ReagentEffectsEntry>(), new UniversalJsonConverter<ReagentEffectsEntry>(),
new UniversalJsonConverter<DamageSpecifier>(), new UniversalJsonConverter<DamageSpecifier>(),
new FixedPointJsonConverter() new FixedPointJsonConverter()

View File

@@ -76,7 +76,7 @@ public sealed class ReactionEntry
proto.Products proto.Products
.Select(x => KeyValuePair.Create(x.Key, x.Value.Float())) .Select(x => KeyValuePair.Create(x.Key, x.Value.Float()))
.ToDictionary(x => x.Key, x => x.Value); .ToDictionary(x => x.Key, x => x.Value);
Effects = proto.Effects; Effects = proto.Effects.ToList();
} }
} }

View File

@@ -41,6 +41,7 @@ using Content.Shared.Stacks;
using Content.Server.Construction.Components; using Content.Server.Construction.Components;
using Content.Shared.Chat; using Content.Shared.Chat;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.Temperature.Components;
using Robust.Shared.Utility; using Robust.Shared.Utility;
namespace Content.Server.Kitchen.EntitySystems namespace Content.Server.Kitchen.EntitySystems

View File

@@ -5,12 +5,12 @@ using Content.Server.Medical.Components;
using Content.Server.NodeContainer.EntitySystems; using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.NodeGroups; using Content.Server.NodeContainer.NodeGroups;
using Content.Server.NodeContainer.Nodes; using Content.Server.NodeContainer.Nodes;
using Content.Server.Temperature.Components;
using Content.Shared.Atmos; using Content.Shared.Atmos;
using Content.Shared.Body.Components; using Content.Shared.Body.Components;
using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Medical.Cryogenics; using Content.Shared.Medical.Cryogenics;
using Content.Shared.MedicalScanner; using Content.Shared.MedicalScanner;
using Content.Shared.Temperature.Components;
using Content.Shared.UserInterface; using Content.Shared.UserInterface;
using Robust.Shared.Containers; using Robust.Shared.Containers;

View File

@@ -1,6 +1,5 @@
using Content.Server.Medical.Components; using Content.Server.Medical.Components;
using Content.Server.PowerCell; using Content.Server.PowerCell;
using Content.Server.Temperature.Components;
using Content.Shared.Body.Components; using Content.Shared.Body.Components;
using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Damage; using Content.Shared.Damage;
@@ -13,6 +12,7 @@ using Content.Shared.Item.ItemToggle.Components;
using Content.Shared.MedicalScanner; using Content.Shared.MedicalScanner;
using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Components;
using Content.Shared.Popups; using Content.Shared.Popups;
using Content.Shared.Temperature.Components;
using Content.Shared.Traits.Assorted; using Content.Shared.Traits.Assorted;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
using Robust.Shared.Audio.Systems; using Robust.Shared.Audio.Systems;

View File

@@ -5,7 +5,6 @@ using Content.Server.NPC.Queries.Considerations;
using Content.Server.NPC.Queries.Curves; using Content.Server.NPC.Queries.Curves;
using Content.Server.NPC.Queries.Queries; using Content.Server.NPC.Queries.Queries;
using Content.Server.Nutrition.Components; using Content.Server.Nutrition.Components;
using Content.Server.Temperature.Components;
using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.Examine; using Content.Shared.Examine;
@@ -30,6 +29,7 @@ using Robust.Shared.Prototypes;
using Robust.Shared.Utility; using Robust.Shared.Utility;
using Content.Shared.Atmos.Components; using Content.Shared.Atmos.Components;
using System.Linq; using System.Linq;
using Content.Shared.Temperature.Components;
namespace Content.Server.NPC.Systems; namespace Content.Server.NPC.Systems;

View File

@@ -10,14 +10,14 @@ using Content.Shared.Database;
using Content.Shared.Inventory; using Content.Shared.Inventory;
using Content.Shared.Rejuvenate; using Content.Shared.Rejuvenate;
using Content.Shared.Temperature; using Content.Shared.Temperature;
using Robust.Shared.Physics.Components;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Physics.Events;
using Content.Shared.Projectiles; using Content.Shared.Projectiles;
using Content.Shared.Temperature.Components;
using Content.Shared.Temperature.Systems;
namespace Content.Server.Temperature.Systems; namespace Content.Server.Temperature.Systems;
public sealed class TemperatureSystem : EntitySystem public sealed class TemperatureSystem : SharedTemperatureSystem
{ {
[Dependency] private readonly AlertsSystem _alerts = default!; [Dependency] private readonly AlertsSystem _alerts = default!;
[Dependency] private readonly AtmosphereSystem _atmosphere = default!; [Dependency] private readonly AtmosphereSystem _atmosphere = default!;
@@ -125,8 +125,7 @@ public sealed class TemperatureSystem : EntitySystem
true); true);
} }
public void ChangeHeat(EntityUid uid, float heatAmount, bool ignoreHeatResistance = false, public override void ChangeHeat(EntityUid uid, float heatAmount, bool ignoreHeatResistance = false, TemperatureComponent? temperature = null)
TemperatureComponent? temperature = null)
{ {
if (!Resolve(uid, ref temperature, false)) if (!Resolve(uid, ref temperature, false))
return; return;
@@ -161,16 +160,6 @@ public sealed class TemperatureSystem : EntitySystem
ChangeHeat(uid, heat * temperature.AtmosTemperatureTransferEfficiency, temperature: temperature); ChangeHeat(uid, heat * temperature.AtmosTemperatureTransferEfficiency, temperature: temperature);
} }
public float GetHeatCapacity(EntityUid uid, TemperatureComponent? comp = null, PhysicsComponent? physics = null)
{
if (!Resolve(uid, ref comp) || !Resolve(uid, ref physics, false) || physics.FixturesMass <= 0)
{
return Atmospherics.MinimumHeatCapacity;
}
return comp.SpecificHeat * physics.FixturesMass;
}
private void OnInit(EntityUid uid, InternalTemperatureComponent comp, MapInitEvent args) private void OnInit(EntityUid uid, InternalTemperatureComponent comp, MapInitEvent args)
{ {
if (!TryComp<TemperatureComponent>(uid, out var temp)) if (!TryComp<TemperatureComponent>(uid, out var temp))

View File

@@ -14,6 +14,6 @@ public sealed partial class TileEntityEffectComponent : Component
/// <summary> /// <summary>
/// List of effects that should be applied. /// List of effects that should be applied.
/// </summary> /// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField] [DataField]
public List<EntityEffect> Effects = default!; public List<EntityEffect> Effects = default!;
} }

View File

@@ -1,13 +1,11 @@
using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Shared.StepTrigger.Systems; using Content.Shared.StepTrigger.Systems;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.EntityEffects; using Content.Shared.EntityEffects;
namespace Content.Server.Tiles; namespace Content.Server.Tiles;
public sealed class TileEntityEffectSystem : EntitySystem public sealed class TileEntityEffectSystem : EntitySystem
{ {
[Dependency] private readonly SharedEntityEffectsSystem _entityEffects = default!;
public override void Initialize() public override void Initialize()
{ {
@@ -23,11 +21,7 @@ public sealed class TileEntityEffectSystem : EntitySystem
private void OnTileStepTriggered(Entity<TileEntityEffectComponent> ent, ref StepTriggeredOffEvent args) private void OnTileStepTriggered(Entity<TileEntityEffectComponent> ent, ref StepTriggeredOffEvent args)
{ {
var otherUid = args.Tripper; var otherUid = args.Tripper;
var effectArgs = new EntityEffectBaseArgs(otherUid, EntityManager);
foreach (var effect in ent.Comp.Effects) _entityEffects.ApplyEffects(otherUid, ent.Comp.Effects.ToArray());
{
effect.Effect(effectArgs);
}
} }
} }

View File

@@ -13,7 +13,6 @@ using Content.Server.NPC.HTN;
using Content.Server.NPC.Systems; using Content.Server.NPC.Systems;
using Content.Server.StationEvents.Components; using Content.Server.StationEvents.Components;
using Content.Server.Speech.Components; using Content.Server.Speech.Components;
using Content.Server.Temperature.Components;
using Content.Shared.Body.Components; using Content.Shared.Body.Components;
using Content.Shared.Chat; using Content.Shared.Chat;
using Content.Shared.CombatMode; using Content.Shared.CombatMode;
@@ -44,6 +43,7 @@ using Robust.Shared.Player;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Content.Shared.NPC.Prototypes; using Content.Shared.NPC.Prototypes;
using Content.Shared.Roles; using Content.Shared.Roles;
using Content.Shared.Temperature.Components;
namespace Content.Server.Zombies; namespace Content.Server.Zombies;

View File

@@ -81,9 +81,9 @@ public enum LogType
ChemicalReaction = 17, ChemicalReaction = 17,
/// <summary> /// <summary>
/// Reagent effects related interactions. /// EntityEffect related interactions.
/// </summary> /// </summary>
ReagentEffect = 18, EntityEffect = 18,
/// <summary> /// <summary>
/// Canister valve was opened or closed. /// Canister valve was opened or closed.

View File

@@ -22,7 +22,7 @@ public sealed partial class LungComponent : Component
/// The name/key of the solution on this entity which these lungs act on. /// The name/key of the solution on this entity which these lungs act on.
/// </summary> /// </summary>
[DataField] [DataField]
public string SolutionName = LungSystem.LungSolutionName; public string SolutionName = "Lung";
/// <summary> /// <summary>
/// The solution on this entity that these lungs act on. /// The solution on this entity that these lungs act on.

View File

@@ -1,11 +1,14 @@
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components; using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.EntitySystems; using Content.Shared.Atmos.EntitySystems;
using Content.Shared.Body.Components; using Content.Shared.Body.Components;
using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Body.Prototypes;
using Content.Shared.Atmos;
using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Components;
using Content.Shared.Clothing; using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Inventory.Events; using Content.Shared.Inventory.Events;
using Robust.Shared.Prototypes;
using BreathToolComponent = Content.Shared.Atmos.Components.BreathToolComponent;
using InternalsComponent = Content.Shared.Body.Components.InternalsComponent;
namespace Content.Shared.Body.Systems; namespace Content.Shared.Body.Systems;
@@ -15,8 +18,6 @@ public sealed class LungSystem : EntitySystem
[Dependency] private readonly SharedInternalsSystem _internals = default!; [Dependency] private readonly SharedInternalsSystem _internals = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!; [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
public static string LungSolutionName = "Lung";
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
@@ -53,6 +54,7 @@ public sealed class LungSystem : EntitySystem
} }
} }
// TODO: JUST METABOLIZE GASES DIRECTLY DON'T CONVERT TO REAGENTS!!! (Needs Metabolism refactor :B)
public void GasToReagent(EntityUid uid, LungComponent lung) public void GasToReagent(EntityUid uid, LungComponent lung)
{ {
if (!_solutionContainerSystem.ResolveSolution(uid, lung.SolutionName, ref lung.Solution, out var solution)) if (!_solutionContainerSystem.ResolveSolution(uid, lung.SolutionName, ref lung.Solution, out var solution))

View File

@@ -6,7 +6,8 @@ using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reaction; using Content.Shared.Chemistry.Reaction;
using Content.Shared.Chemistry.Reagent; using Content.Shared.Chemistry.Reagent;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.EntityEffects.Effects; using Content.Shared.EntityEffects.Effects.Solution;
using Content.Shared.EntityEffects.Effects.Transform;
using Content.Shared.FixedPoint; using Content.Shared.FixedPoint;
using Content.Shared.Fluids; using Content.Shared.Fluids;
using Content.Shared.Forensics.Components; using Content.Shared.Forensics.Components;
@@ -149,7 +150,9 @@ public abstract class SharedBloodstreamSystem : EntitySystem
{ {
switch (effect) switch (effect)
{ {
case CreateEntityReactionEffect: // Prevent entities from spawning in the bloodstream // TODO: Rather than this, ReactionAttempt should allow systems to remove effects from the list before the reaction.
// TODO: I think there's a PR up on the repo for this and if there isn't I'll make one -Princess
case EntityEffects.Effects.EntitySpawning.SpawnEntity: // Prevent entities from spawning in the bloodstream
case AreaReactionEffect: // No spontaneous smoke or foam leaking out of blood vessels. case AreaReactionEffect: // No spontaneous smoke or foam leaking out of blood vessels.
args.Cancelled = true; args.Cancelled = true;
return; return;

View File

@@ -707,6 +707,7 @@ public abstract partial class SharedSolutionContainerSystem : EntitySystem
} }
// Thermal energy and temperature management. // Thermal energy and temperature management.
// TODO: ENERGY CONSERVATION!!! Nuke this once we have HeatContainers and use methods which properly conserve energy and model heat transfer correctly!
#region Thermal Energy and Temperature #region Thermal Energy and Temperature
@@ -763,6 +764,26 @@ public abstract partial class SharedSolutionContainerSystem : EntitySystem
UpdateChemicals(soln); UpdateChemicals(soln);
} }
/// <summary>
/// Same as <see cref="AddThermalEnergy"/> but clamps the value between two temperature values.
/// </summary>
/// <param name="soln">Solution we're adjusting the energy of</param>
/// <param name="thermalEnergy">Thermal energy we're adding or removing</param>
/// <param name="min">Min desired temperature</param>
/// <param name="max">Max desired temperature</param>
public void AddThermalEnergyClamped(Entity<SolutionComponent> soln, float thermalEnergy, float min, float max)
{
var solution = soln.Comp.Solution;
if (thermalEnergy == 0.0f)
return;
var heatCap = solution.GetHeatCapacity(PrototypeManager);
var deltaT = thermalEnergy / heatCap;
solution.Temperature = Math.Clamp(solution.Temperature + deltaT, min, max);
UpdateChemicals(soln);
}
#endregion Thermal Energy and Temperature #endregion Thermal Energy and Temperature
#region Event Handlers #region Event Handlers

View File

@@ -31,6 +31,7 @@ namespace Content.Shared.Chemistry.Reaction
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedTransformSystem _transformSystem = default!; [Dependency] private readonly SharedTransformSystem _transformSystem = default!;
[Dependency] private readonly SharedEntityEffectsSystem _entityEffects = default!;
/// <summary> /// <summary>
/// A cache of all reactions indexed by at most ONE of their required reactants. /// A cache of all reactions indexed by at most ONE of their required reactants.
@@ -205,27 +206,12 @@ namespace Content.Shared.Chemistry.Reaction
private void OnReaction(Entity<SolutionComponent> soln, ReactionPrototype reaction, ReagentPrototype? reagent, FixedPoint2 unitReactions) private void OnReaction(Entity<SolutionComponent> soln, ReactionPrototype reaction, ReagentPrototype? reagent, FixedPoint2 unitReactions)
{ {
var args = new EntityEffectReagentArgs(soln, EntityManager, null, soln.Comp.Solution, unitReactions, reagent, null, 1f);
var posFound = _transformSystem.TryGetMapOrGridCoordinates(soln, out var gridPos); var posFound = _transformSystem.TryGetMapOrGridCoordinates(soln, out var gridPos);
_adminLogger.Add(LogType.ChemicalReaction, reaction.Impact, _adminLogger.Add(LogType.ChemicalReaction, reaction.Impact,
$"Chemical reaction {reaction.ID:reaction} occurred with strength {unitReactions:strength} on entity {ToPrettyString(soln):metabolizer} at Pos:{(posFound ? $"{gridPos:coordinates}" : "[Grid or Map not Found]")}"); $"Chemical reaction {reaction.ID:reaction} occurred with strength {unitReactions:strength} on entity {ToPrettyString(soln):metabolizer} at Pos:{(posFound ? $"{gridPos:coordinates}" : "[Grid or Map not Found]")}");
foreach (var effect in reaction.Effects) _entityEffects.ApplyEffects(soln, reaction.Effects, unitReactions.Float());
{
if (!effect.ShouldApply(args))
continue;
if (effect.ShouldLog)
{
var entity = args.TargetEntity;
_adminLogger.Add(LogType.ReagentEffect, effect.LogImpact,
$"Reaction effect {effect.GetType().Name:effect} of reaction {reaction.ID:reaction} applied on entity {ToPrettyString(entity):entity} at Pos:{(posFound ? $"{gridPos:coordinates}" : "[Grid or Map not Found")}");
}
effect.Effect(args);
}
// Someday, some brave soul will thread through an optional actor // Someday, some brave soul will thread through an optional actor
// argument in from every call of OnReaction up, all just to pass // argument in from every call of OnReaction up, all just to pass

View File

@@ -60,7 +60,7 @@ namespace Content.Shared.Chemistry.Reaction
/// <summary> /// <summary>
/// Effects to be triggered when the reaction occurs. /// Effects to be triggered when the reaction occurs.
/// </summary> /// </summary>
[DataField("effects")] public List<EntityEffect> Effects = new(); [DataField("effects")] public EntityEffect[] Effects = [];
/// <summary> /// <summary>
/// How dangerous is this effect? Stuff like bicaridine should be low, while things like methamphetamine /// How dangerous is this effect? Stuff like bicaridine should be low, while things like methamphetamine

View File

@@ -34,7 +34,7 @@ public sealed partial class ReactiveReagentEffectEntry
public HashSet<string>? Reagents = null; public HashSet<string>? Reagents = null;
[DataField("effects", required: true)] [DataField("effects", required: true)]
public List<EntityEffect> Effects = default!; public EntityEffect[] Effects = default!;
[DataField("groups", readOnly: true, serverOnly: true, [DataField("groups", readOnly: true, serverOnly: true,
customTypeSerializer:typeof(PrototypeIdDictionarySerializer<HashSet<ReactionMethod>, ReactiveGroupPrototype>))] customTypeSerializer:typeof(PrototypeIdDictionarySerializer<HashSet<ReactionMethod>, ReactiveGroupPrototype>))]

View File

@@ -1,108 +1,35 @@
using Content.Shared.Administration.Logs;
using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reaction;
using Content.Shared.Chemistry.Reagent; using Content.Shared.Chemistry.Reagent;
using Content.Shared.Database; using Content.Shared.FixedPoint;
using Content.Shared.EntityEffects;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Shared.Chemistry; namespace Content.Shared.Chemistry;
[UsedImplicitly] [UsedImplicitly]
public sealed class ReactiveSystem : EntitySystem public sealed class ReactiveSystem : EntitySystem
{ {
[Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IRobustRandom _robustRandom = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
public void DoEntityReaction(EntityUid uid, Solution solution, ReactionMethod method) public void DoEntityReaction(EntityUid uid, Solution solution, ReactionMethod method)
{ {
foreach (var reagent in solution.Contents.ToArray()) foreach (var reagent in solution.Contents.ToArray())
{ {
ReactionEntity(uid, method, reagent, solution); ReactionEntity(uid, method, reagent);
} }
} }
public void ReactionEntity(EntityUid uid, ReactionMethod method, ReagentQuantity reagentQuantity, Solution? source) public void ReactionEntity(EntityUid uid, ReactionMethod method, ReagentQuantity reagentQuantity)
{ {
// We throw if the reagent specified doesn't exist. if (reagentQuantity.Quantity == FixedPoint2.Zero)
var proto = _prototypeManager.Index<ReagentPrototype>(reagentQuantity.Reagent.Prototype);
ReactionEntity(uid, method, proto, reagentQuantity, source);
}
public void ReactionEntity(EntityUid uid, ReactionMethod method, ReagentPrototype proto,
ReagentQuantity reagentQuantity, Solution? source)
{
if (!TryComp(uid, out ReactiveComponent? reactive))
return; return;
// custom event for bypassing reactivecomponent stuff // We throw if the reagent specified doesn't exist.
var ev = new ReactionEntityEvent(method, proto, reagentQuantity, source); if (!_proto.Resolve<ReagentPrototype>(reagentQuantity.Reagent.Prototype, out var proto))
return;
var ev = new ReactionEntityEvent(method, reagentQuantity, proto);
RaiseLocalEvent(uid, ref ev); RaiseLocalEvent(uid, ref ev);
// If we have a source solution, use the reagent quantity we have left. Otherwise, use the reaction volume specified.
var args = new EntityEffectReagentArgs(uid, EntityManager, null, source, source?.GetReagentQuantity(reagentQuantity.Reagent) ?? reagentQuantity.Quantity, proto, method, 1f);
// First, check if the reagent wants to apply any effects.
if (proto.ReactiveEffects != null && reactive.ReactiveGroups != null)
{
foreach (var (key, val) in proto.ReactiveEffects)
{
if (!val.Methods.Contains(method))
continue;
if (!reactive.ReactiveGroups.ContainsKey(key))
continue;
if (!reactive.ReactiveGroups[key].Contains(method))
continue;
foreach (var effect in val.Effects)
{
if (!effect.ShouldApply(args, _robustRandom))
continue;
if (effect.ShouldLog)
{
var entity = args.TargetEntity;
_adminLogger.Add(LogType.ReagentEffect, effect.LogImpact,
$"Reactive effect {effect.GetType().Name:effect} of reagent {proto.ID:reagent} with method {method} applied on entity {ToPrettyString(entity):entity} at {Transform(entity).Coordinates:coordinates}");
}
effect.Effect(args);
}
}
}
// Then, check if the prototype has any effects it can apply as well.
if (reactive.Reactions != null)
{
foreach (var entry in reactive.Reactions)
{
if (!entry.Methods.Contains(method))
continue;
if (entry.Reagents != null && !entry.Reagents.Contains(proto.ID))
continue;
foreach (var effect in entry.Effects)
{
if (!effect.ShouldApply(args, _robustRandom))
continue;
if (effect.ShouldLog)
{
var entity = args.TargetEntity;
_adminLogger.Add(LogType.ReagentEffect, effect.LogImpact,
$"Reactive effect {effect.GetType().Name:effect} of {ToPrettyString(entity):entity} using reagent {proto.ID:reagent} with method {method} at {Transform(entity).Coordinates:coordinates}");
}
effect.Effect(args);
}
}
}
} }
} }
public enum ReactionMethod public enum ReactionMethod
@@ -113,9 +40,4 @@ Ingestion,
} }
[ByRefEvent] [ByRefEvent]
public readonly record struct ReactionEntityEvent( public readonly record struct ReactionEntityEvent(ReactionMethod Method, ReagentQuantity ReagentQuantity, ReagentPrototype Reagent);
ReactionMethod Method,
ReagentPrototype Reagent,
ReagentQuantity ReagentQuantity,
Solution? Source
);

View File

@@ -2,21 +2,17 @@ using System.Collections.Frozen;
using System.Linq; using System.Linq;
using Content.Shared.FixedPoint; using Content.Shared.FixedPoint;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using Content.Shared.Administration.Logs;
using Content.Shared.Body.Prototypes; using Content.Shared.Body.Prototypes;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reaction; using Content.Shared.Chemistry.Reaction;
using Content.Shared.Contraband; using Content.Shared.Contraband;
using Content.Shared.EntityEffects; using Content.Shared.EntityEffects;
using Content.Shared.Database; using Content.Shared.Localizations;
using Content.Shared.Nutrition; using Content.Shared.Nutrition;
using Content.Shared.Prototypes;
using Content.Shared.Roles; using Content.Shared.Roles;
using Content.Shared.Slippery; using Content.Shared.Slippery;
using Robust.Shared.Audio; using Robust.Shared.Audio;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Serialization; using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array;
using Robust.Shared.Utility; using Robust.Shared.Utility;
@@ -190,6 +186,7 @@ namespace Content.Shared.Chemistry.Reagent
[DataField] [DataField]
public SoundSpecifier FootstepSound = new SoundCollectionSpecifier("FootstepPuddle"); public SoundSpecifier FootstepSound = new SoundCollectionSpecifier("FootstepPuddle");
// TODO: Reaction tile doesn't work properly and destroys reagents way too quickly
public FixedPoint2 ReactionTile(TileRef tile, FixedPoint2 reactVolume, IEntityManager entityManager, List<ReagentData>? data) public FixedPoint2 ReactionTile(TileRef tile, FixedPoint2 reactVolume, IEntityManager entityManager, List<ReagentData>? data)
{ {
var removed = FixedPoint2.Zero; var removed = FixedPoint2.Zero;
@@ -211,33 +208,32 @@ namespace Content.Shared.Chemistry.Reagent
return removed; return removed;
} }
public void ReactionPlant(EntityUid? plantHolder, public IEnumerable<string> GuidebookReagentEffectsDescription(IPrototypeManager prototype, IEntitySystemManager entSys, IEnumerable<EntityEffect> effects, FixedPoint2? metabolism = null)
ReagentQuantity amount,
Solution solution,
EntityManager entityManager,
IRobustRandom random,
ISharedAdminLogManager logger)
{ {
if (plantHolder == null) return effects.Select(x => GuidebookReagentEffectDescription(prototype, entSys, x, metabolism))
return; .Where(x => x is not null)
.Select(x => x!)
.ToArray();
}
var args = new EntityEffectReagentArgs(plantHolder.Value, entityManager, null, solution, amount.Quantity, this, null, 1f); public string? GuidebookReagentEffectDescription(IPrototypeManager prototype, IEntitySystemManager entSys, EntityEffect effect, FixedPoint2? metabolism)
foreach (var plantMetabolizable in PlantMetabolisms) {
{ if (effect.EntityEffectGuidebookText(prototype, entSys) is not { } description)
if (!plantMetabolizable.ShouldApply(args, random)) return null;
continue;
if (plantMetabolizable.ShouldLog) var quantity = metabolism == null ? 0f : (double) (effect.MinScale * metabolism);
{
var entity = args.TargetEntity;
logger.Add(
LogType.ReagentEffect,
plantMetabolizable.LogImpact,
$"Plant metabolism effect {plantMetabolizable.GetType().Name:effect} of reagent {ID} applied on entity {entity}");
}
plantMetabolizable.Effect(args); return Loc.GetString(
} "guidebook-reagent-effect-description",
("reagent", LocalizedName),
("quantity", quantity),
("effect", description),
("chance", effect.Probability),
("conditionCount", effect.Conditions?.Length ?? 0),
("conditions",
ContentLocalizationManager.FormatList(
effect.Conditions?.Select(x => x.EntityConditionGuidebookText(prototype)).ToList() ?? new List<string>()
)));
} }
} }
@@ -246,6 +242,7 @@ namespace Content.Shared.Chemistry.Reagent
{ {
public string ReagentPrototype; public string ReagentPrototype;
// TODO: Kill Metabolism groups!
public Dictionary<ProtoId<MetabolismGroupPrototype>, ReagentEffectsGuideEntry>? GuideEntries; public Dictionary<ProtoId<MetabolismGroupPrototype>, ReagentEffectsGuideEntry>? GuideEntries;
public List<string>? PlantMetabolisms = null; public List<string>? PlantMetabolisms = null;
@@ -254,15 +251,12 @@ namespace Content.Shared.Chemistry.Reagent
{ {
ReagentPrototype = proto.ID; ReagentPrototype = proto.ID;
GuideEntries = proto.Metabolisms? GuideEntries = proto.Metabolisms?
.Select(x => (x.Key, x.Value.MakeGuideEntry(prototype, entSys))) .Select(x => (x.Key, x.Value.MakeGuideEntry(prototype, entSys, proto)))
.ToDictionary(x => x.Key, x => x.Item2); .ToDictionary(x => x.Key, x => x.Item2);
if (proto.PlantMetabolisms.Count > 0) if (proto.PlantMetabolisms.Count > 0)
{ {
PlantMetabolisms = new List<string>(proto.PlantMetabolisms PlantMetabolisms =
.Select(x => x.GuidebookEffectDescription(prototype, entSys)) new List<string>(proto.GuidebookReagentEffectsDescription(prototype, entSys, proto.PlantMetabolisms));
.Where(x => x is not null)
.Select(x => x!)
.ToArray());
} }
} }
} }
@@ -285,14 +279,11 @@ namespace Content.Shared.Chemistry.Reagent
[DataField("effects", required: true)] [DataField("effects", required: true)]
public EntityEffect[] Effects = default!; public EntityEffect[] Effects = default!;
public ReagentEffectsGuideEntry MakeGuideEntry(IPrototypeManager prototype, IEntitySystemManager entSys) public string EntityEffectFormat => "guidebook-reagent-effect-description";
public ReagentEffectsGuideEntry MakeGuideEntry(IPrototypeManager prototype, IEntitySystemManager entSys, ReagentPrototype proto)
{ {
return new ReagentEffectsGuideEntry(MetabolismRate, return new ReagentEffectsGuideEntry(MetabolismRate, proto.GuidebookReagentEffectsDescription(prototype, entSys, Effects, MetabolismRate).ToArray());
Effects
.Select(x => x.GuidebookEffectDescription(prototype, entSys)) // hate.
.Where(x => x is not null)
.Select(x => x!)
.ToArray());
} }
} }

View File

@@ -0,0 +1,10 @@
using Robust.Shared.Prototypes;
namespace Content.Shared.EntityConditions.Conditions.Body;
/// <inheritdoc cref="EntityCondition"/>
public sealed partial class BreathingCondition : EntityConditionBase<BreathingCondition>
{
public override string EntityConditionGuidebookText(IPrototypeManager prototype) =>
Loc.GetString("reagent-effect-condition-guidebook-breathing", ("isBreathing", !Inverted));
}

View File

@@ -0,0 +1,33 @@
using Content.Shared.Nutrition.Components;
using Content.Shared.Nutrition.EntitySystems;
using Robust.Shared.Prototypes;
namespace Content.Shared.EntityConditions.Conditions.Body;
/// <summary>
/// Returns true if this entity's hunger is within a specified minimum and maximum.
/// </summary>
/// <inheritdoc cref="EntityConditionSystem{T, TCondition}"/>
public sealed partial class TotalHungerEntityConditionSystem : EntityConditionSystem<HungerComponent, HungerCondition>
{
[Dependency] private readonly HungerSystem _hunger = default!;
protected override void Condition(Entity<HungerComponent> entity, ref EntityConditionEvent<HungerCondition> args)
{
var total = _hunger.GetHunger(entity.Comp);
args.Result = total >= args.Condition.Min && total <= args.Condition.Max;
}
}
/// <inheritdoc cref="EntityCondition"/>
public sealed partial class HungerCondition : EntityConditionBase<HungerCondition>
{
[DataField]
public float Min;
[DataField]
public float Max = float.PositiveInfinity;
public override string EntityConditionGuidebookText(IPrototypeManager prototype) =>
Loc.GetString("reagent-effect-condition-guidebook-total-hunger", ("max", float.IsPositiveInfinity(Max) ? int.MaxValue : Max), ("min", Min));
}

View File

@@ -0,0 +1,23 @@
using Content.Shared.Body.Components;
using Robust.Shared.Prototypes;
namespace Content.Shared.EntityConditions.Conditions.Body;
/// <summary>
/// Returns true if this entity is using internals. False if they are not or cannot use internals.
/// </summary>
/// <inheritdoc cref="EntityConditionSystem{T, TCondition}"/>
public sealed partial class InternalsOnEntityConditionSystem : EntityConditionSystem<InternalsComponent, InternalsCondition>
{
protected override void Condition(Entity<InternalsComponent> entity, ref EntityConditionEvent<InternalsCondition> args)
{
args.Result = entity.Comp.GasTankEntity != null;
}
}
/// <inheritdoc cref="EntityCondition"/>
public sealed partial class InternalsCondition : EntityConditionBase<InternalsCondition>
{
public override string EntityConditionGuidebookText(IPrototypeManager prototype) =>
Loc.GetString("reagent-effect-condition-guidebook-internals", ("usingInternals", !Inverted));
}

View File

@@ -0,0 +1,31 @@
using Content.Shared.Body.Prototypes;
using Content.Shared.Localizations;
using Robust.Shared.Prototypes;
namespace Content.Shared.EntityConditions.Conditions.Body;
/// <inheritdoc cref="EntityCondition"/>
public sealed partial class MetabolizerTypeCondition : EntityConditionBase<MetabolizerTypeCondition>
{
[DataField(required: true)]
public ProtoId<MetabolizerTypePrototype>[] Type = default!;
public override string EntityConditionGuidebookText(IPrototypeManager prototype)
{
var typeList = new List<string>();
foreach (var type in Type)
{
if (!prototype.Resolve(type, out var proto))
continue;
typeList.Add(proto.LocalizedName);
}
var names = ContentLocalizationManager.FormatListToOr(typeList);
return Loc.GetString("reagent-effect-condition-guidebook-organ-type",
("name", names),
("shouldhave", !Inverted));
}
}

View File

@@ -0,0 +1,28 @@
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Robust.Shared.Prototypes;
namespace Content.Shared.EntityConditions.Conditions.Body;
/// <summary>
/// Returns true if this entity's current mob state matches the condition's specified mob state.
/// </summary>
/// <inheritdoc cref="EntityConditionSystem{T, TCondition}"/>
public sealed partial class MobStateEntityConditionSystem : EntityConditionSystem<MobStateComponent, MobStateCondition>
{
protected override void Condition(Entity<MobStateComponent> entity, ref EntityConditionEvent<MobStateCondition> args)
{
if (entity.Comp.CurrentState == args.Condition.Mobstate)
args.Result = true;
}
}
/// <inheritdoc cref="EntityCondition"/>
public sealed partial class MobStateCondition : EntityConditionBase<MobStateCondition>
{
[DataField]
public MobState Mobstate = MobState.Alive;
public override string EntityConditionGuidebookText(IPrototypeManager prototype) =>
Loc.GetString("reagent-effect-condition-guidebook-mob-state-condition", ("state", Mobstate));
}

View File

@@ -0,0 +1,59 @@
using System.Linq;
using Content.Shared.Localizations;
using Content.Shared.Mind;
using Content.Shared.Mind.Components;
using Content.Shared.Roles;
using Content.Shared.Roles.Components;
using Robust.Shared.Prototypes;
namespace Content.Shared.EntityConditions.Conditions;
/// <summary>
/// Returns true if this entity has any of the specified jobs. False if the entity has no mind, none of the specified jobs, or is jobless.
/// </summary>
/// <inheritdoc cref="EntityConditionSystem{T, TCondition}"/>
public sealed partial class HasJobEntityConditionSystem : EntityConditionSystem<MindContainerComponent, JobCondition>
{
protected override void Condition(Entity<MindContainerComponent> entity, ref EntityConditionEvent<JobCondition> args)
{
// We need a mind in our mind container...
if (!TryComp<MindComponent>(entity.Comp.Mind, out var mind))
return;
foreach (var roleId in mind.MindRoleContainer.ContainedEntities)
{
if (!HasComp<JobRoleComponent>(roleId))
continue;
if (!TryComp<MindRoleComponent>(roleId, out var mindRole))
{
Log.Error($"Encountered job mind role entity {roleId} without a {nameof(MindRoleComponent)}");
continue;
}
if (mindRole.JobPrototype == null)
{
Log.Error($"Encountered job mind role entity {roleId} without a {nameof(JobPrototype)}");
continue;
}
if (!args.Condition.Jobs.Contains(mindRole.JobPrototype.Value))
continue;
args.Result = true;
return;
}
}
}
/// <inheritdoc cref="EntityCondition"/>
public sealed partial class JobCondition : EntityConditionBase<JobCondition>
{
[DataField(required: true)] public List<ProtoId<JobPrototype>> Jobs = [];
public override string EntityConditionGuidebookText(IPrototypeManager prototype)
{
var localizedNames = Jobs.Select(jobId => prototype.Index(jobId).LocalizedName).ToList();
return Loc.GetString("reagent-effect-condition-guidebook-job-condition", ("job", ContentLocalizationManager.FormatListToOr(localizedNames)));
}
}

View File

@@ -0,0 +1,44 @@
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.FixedPoint;
using Robust.Shared.Prototypes;
namespace Content.Shared.EntityConditions.Conditions;
/// <summary>
/// Returns true if this solution entity has an amount of reagent in it within a specified minimum and maximum.
/// </summary>
/// <inheritdoc cref="EntityConditionSystem{T, TCondition}"/>
public sealed partial class ReagentThresholdEntityConditionSystem : EntityConditionSystem<SolutionComponent, ReagentCondition>
{
protected override void Condition(Entity<SolutionComponent> entity, ref EntityConditionEvent<ReagentCondition> args)
{
var quant = entity.Comp.Solution.GetTotalPrototypeQuantity(args.Condition.Reagent);
args.Result = quant >= args.Condition.Min && quant <= args.Condition.Max;
}
}
/// <inheritdoc cref="EntityCondition"/>
public sealed partial class ReagentCondition : EntityConditionBase<ReagentCondition>
{
[DataField]
public FixedPoint2 Min = FixedPoint2.Zero;
[DataField]
public FixedPoint2 Max = FixedPoint2.MaxValue;
[DataField(required: true)]
public ProtoId<ReagentPrototype> Reagent;
public override string EntityConditionGuidebookText(IPrototypeManager prototype)
{
if (!prototype.Resolve(Reagent, out var reagentProto))
return String.Empty;
return Loc.GetString("reagent-effect-condition-guidebook-reagent-threshold",
("reagent", reagentProto.LocalizedName ?? Loc.GetString("reagent-effect-condition-guidebook-this-reagent")),
("max", Max == FixedPoint2.MaxValue ? int.MaxValue : Max.Float()),
("min", Min.Float()));
}
}

View File

@@ -0,0 +1,43 @@
using Content.Shared.Localizations;
using Content.Shared.Tag;
using Robust.Shared.Prototypes;
namespace Content.Shared.EntityConditions.Conditions.Tags;
/// <summary>
/// Returns true if this entity has all the listed tags.
/// </summary>
/// <inheritdoc cref="EntityConditionSystem{T, TCondition}"/>
public sealed partial class HasAllTagsEntityConditionSystem : EntityConditionSystem<TagComponent, AllTagsCondition>
{
[Dependency] private readonly TagSystem _tag = default!;
protected override void Condition(Entity<TagComponent> entity, ref EntityConditionEvent<AllTagsCondition> args)
{
args.Result = _tag.HasAllTags(entity.Comp, args.Condition.Tags);
}
}
/// <inheritdoc cref="EntityCondition"/>
public sealed partial class AllTagsCondition : EntityConditionBase<AllTagsCondition>
{
[DataField(required: true)]
public ProtoId<TagPrototype>[] Tags = [];
public override string EntityConditionGuidebookText(IPrototypeManager prototype)
{
var tagList = new List<string>();
foreach (var type in Tags)
{
if (!prototype.Resolve(type, out var proto))
continue;
tagList.Add(proto.ID);
}
var names = ContentLocalizationManager.FormatList(tagList);
return Loc.GetString("reagent-effect-condition-guidebook-has-tag", ("tag", names), ("invert", Inverted));
}
}

View File

@@ -0,0 +1,43 @@
using Content.Shared.Localizations;
using Content.Shared.Tag;
using Robust.Shared.Prototypes;
namespace Content.Shared.EntityConditions.Conditions.Tags;
/// <summary>
/// Returns true if this entity have any of the listed tags.
/// </summary>
/// <inheritdoc cref="EntityConditionSystem{T, TCondition}"/>
public sealed partial class HasAnyTagEntityConditionSystem : EntityConditionSystem<TagComponent, AnyTagCondition>
{
[Dependency] private readonly TagSystem _tag = default!;
protected override void Condition(Entity<TagComponent> entity, ref EntityConditionEvent<AnyTagCondition> args)
{
args.Result = _tag.HasAnyTag(entity.Comp, args.Condition.Tags);
}
}
/// <inheritdoc cref="EntityCondition"/>
public sealed partial class AnyTagCondition : EntityConditionBase<AnyTagCondition>
{
[DataField(required: true)]
public ProtoId<TagPrototype>[] Tags = [];
public override string EntityConditionGuidebookText(IPrototypeManager prototype)
{
var tagList = new List<string>();
foreach (var type in Tags)
{
if (!prototype.Resolve(type, out var proto))
continue;
tagList.Add(proto.ID);
}
var names = ContentLocalizationManager.FormatListToOr(tagList);
return Loc.GetString("reagent-effect-condition-guidebook-has-tag", ("tag", names), ("invert", Inverted));
}
}

View File

@@ -0,0 +1,28 @@
using Content.Shared.Tag;
using Robust.Shared.Prototypes;
namespace Content.Shared.EntityConditions.Conditions.Tags;
/// <summary>
/// Returns true if this entity has the listed tag.
/// </summary>
/// <inheritdoc cref="EntityConditionSystem{T, TCondition}"/>
public sealed partial class HasTagEntityConditionSystem : EntityConditionSystem<TagComponent, TagCondition>
{
[Dependency] private readonly TagSystem _tag = default!;
protected override void Condition(Entity<TagComponent> entity, ref EntityConditionEvent<TagCondition> args)
{
args.Result = _tag.HasTag(entity.Comp, args.Condition.Tag);
}
}
/// <inheritdoc cref="EntityCondition"/>
public sealed partial class TagCondition : EntityConditionBase<TagCondition>
{
[DataField(required: true)]
public ProtoId<TagPrototype> Tag;
public override string EntityConditionGuidebookText(IPrototypeManager prototype) =>
Loc.GetString("reagent-effect-condition-guidebook-has-tag", ("tag", Tag), ("invert", Inverted));
}

View File

@@ -0,0 +1,52 @@
using Content.Shared.Chemistry.Components;
using Content.Shared.Temperature.Components;
using Robust.Shared.Prototypes;
namespace Content.Shared.EntityConditions.Conditions;
/// <summary>
/// Returns true if this entity has an amount of reagent in it within a specified minimum and maximum.
/// </summary>
/// <inheritdoc cref="EntityConditionSystem{T, TCondition}"/>
public sealed partial class TemperatureEntityConditionSystem : EntityConditionSystem<TemperatureComponent, TemperatureCondition>
{
protected override void Condition(Entity<TemperatureComponent> entity, ref EntityConditionEvent<TemperatureCondition> args)
{
if (entity.Comp.CurrentTemperature >= args.Condition.Min && entity.Comp.CurrentTemperature <= args.Condition.Max)
args.Result = true;
}
}
/// <summary>
/// Returns true if this solution entity has an amount of reagent in it within a specified minimum and maximum.
/// </summary>
/// <inheritdoc cref="EntityConditionSystem{T, TCondition}"/>
public sealed partial class SolutionTemperatureEntityConditionSystem : EntityConditionSystem<SolutionComponent, TemperatureCondition>
{
protected override void Condition(Entity<SolutionComponent> entity, ref EntityConditionEvent<TemperatureCondition> args)
{
if (entity.Comp.Solution.Temperature >= args.Condition.Min && entity.Comp.Solution.Temperature <= args.Condition.Max)
args.Result = true;
}
}
/// <inheritdoc cref="EntityCondition"/>
public sealed partial class TemperatureCondition : EntityConditionBase<TemperatureCondition>
{
/// <summary>
/// Minimum allowed temperature
/// </summary>
[DataField]
public float Min = 0;
/// <summary>
/// Maximum allowed temperature
/// </summary>
[DataField]
public float Max = float.PositiveInfinity;
public override string EntityConditionGuidebookText(IPrototypeManager prototype) =>
Loc.GetString("reagent-effect-condition-guidebook-body-temperature",
("max", float.IsPositiveInfinity(Max) ? (float) int.MaxValue : Max),
("min", Min));
}

View File

@@ -0,0 +1,20 @@
using Robust.Shared.Prototypes;
namespace Content.Shared.EntityConditions.Conditions;
///<summary>
/// A basic summary of this condition.
/// </summary>
/// <inheritdoc cref="EntityConditionSystem{T, TCondition}"/>
public sealed partial class TemplateEntityConditionSystem : EntityConditionSystem<MetaDataComponent, TemplateCondition>
{
protected override void Condition(Entity<MetaDataComponent> entity, ref EntityConditionEvent<TemplateCondition> args)
{
// Condition goes here.
}
}
/// <inheritdoc cref="EntityCondition"/>
public sealed partial class TemplateCondition : EntityConditionBase<TemplateCondition>
{
public override string EntityConditionGuidebookText(IPrototypeManager prototype) => String.Empty;
}

View File

@@ -0,0 +1,33 @@
using Content.Shared.Damage;
using Content.Shared.FixedPoint;
using Robust.Shared.Prototypes;
namespace Content.Shared.EntityConditions.Conditions;
/// <summary>
/// Returns true if this entity can take damage and if its total damage is within a specified minimum and maximum.
/// </summary>
/// <inheritdoc cref="EntityConditionSystem{T, TCondition}"/>
public sealed partial class TotalDamageEntityConditionSystem : EntityConditionSystem<DamageableComponent, TotalDamageCondition>
{
protected override void Condition(Entity<DamageableComponent> entity, ref EntityConditionEvent<TotalDamageCondition> args)
{
var total = entity.Comp.TotalDamage;
args.Result = total >= args.Condition.Min && total <= args.Condition.Max;
}
}
/// <inheritdoc cref="EntityCondition"/>
public sealed partial class TotalDamageCondition : EntityConditionBase<TotalDamageCondition>
{
[DataField]
public FixedPoint2 Max = FixedPoint2.MaxValue;
[DataField]
public FixedPoint2 Min = FixedPoint2.Zero;
public override string EntityConditionGuidebookText(IPrototypeManager prototype) =>
Loc.GetString("reagent-effect-condition-guidebook-total-damage",
("max", Max == FixedPoint2.MaxValue ? int.MaxValue : Max.Float()),
("min", Min.Float()));
}

View File

@@ -0,0 +1,152 @@
using Robust.Shared.Prototypes;
namespace Content.Shared.EntityConditions;
/// <summary>
/// This handles entity effects.
/// Specifically it handles the receiving of events for causing entity effects, and provides
/// public API for other systems to take advantage of entity effects.
/// </summary>
public sealed partial class SharedEntityConditionsSystem : EntitySystem, IEntityConditionRaiser
{
/// <summary>
/// Checks a list of conditions to verify that they all return true.
/// </summary>
/// <param name="target">Target entity we're checking conditions on</param>
/// <param name="conditions">Conditions we're checking</param>
/// <returns>Returns true if all conditions return true, false if any fail</returns>
public bool TryConditions(EntityUid target, EntityCondition[]? conditions)
{
// If there's no conditions we can't fail any of them...
if (conditions == null)
return true;
foreach (var condition in conditions)
{
if (!TryCondition(target, condition))
return false;
}
return true;
}
/// <summary>
/// Checks a list of conditions to see if any are true.
/// </summary>
/// <param name="target">Target entity we're checking conditions on</param>
/// <param name="conditions">Conditions we're checking</param>
/// <returns>Returns true if any conditions return true</returns>
public bool TryAnyCondition(EntityUid target, EntityCondition[]? conditions)
{
// If there's no conditions we can't meet any of them...
if (conditions == null)
return false;
foreach (var condition in conditions)
{
if (TryCondition(target, condition))
return true;
}
return false;
}
/// <summary>
/// Checks a single <see cref="EntityCondition"/> on an entity.
/// </summary>
/// <param name="target">Target entity we're checking conditions on</param>
/// <param name="condition">Condition we're checking</param>
/// <returns>Returns true if we meet the condition and false otherwise</returns>
public bool TryCondition(EntityUid target, EntityCondition condition)
{
return condition.Inverted != condition.RaiseEvent(target, this);
}
/// <summary>
/// Raises a condition to an entity. You should not be calling this unless you know what you're doing.
/// </summary>
public bool RaiseConditionEvent<T>(EntityUid target, T effect) where T : EntityConditionBase<T>
{
var effectEv = new EntityConditionEvent<T>(effect);
RaiseLocalEvent(target, ref effectEv);
return effectEv.Result;
}
}
/// <summary>
/// This is a basic abstract entity effect containing all the data an entity effect needs to affect entities with effects...
/// </summary>
/// <typeparam name="T">The Component that is required for the effect</typeparam>
/// <typeparam name="TCon">The Condition we're testing</typeparam>
public abstract partial class EntityConditionSystem<T, TCon> : EntitySystem where T : Component where TCon : EntityConditionBase<TCon>
{
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<T, EntityConditionEvent<TCon>>(Condition);
}
protected abstract void Condition(Entity<T> entity, ref EntityConditionEvent<TCon> args);
}
/// <summary>
/// Used to raise an EntityCondition without losing the type of condition.
/// </summary>
public interface IEntityConditionRaiser
{
bool RaiseConditionEvent<T>(EntityUid target, T effect) where T : EntityConditionBase<T>;
}
/// <summary>
/// Used to store an <see cref="EntityCondition"/> so it can be raised without losing the type of the condition.
/// </summary>
/// <typeparam name="T">The Condition wer are raising.</typeparam>
public abstract partial class EntityConditionBase<T> : EntityCondition where T : EntityConditionBase<T>
{
public override bool RaiseEvent(EntityUid target, IEntityConditionRaiser raiser)
{
if (this is not T type)
return false;
// If the result of the event matches the result we're looking for then we pass.
return raiser.RaiseConditionEvent(target, type);
}
}
/// <summary>
/// A basic condition which can be checked for on an entity via events.
/// </summary>
[ImplicitDataDefinitionForInheritors]
public abstract partial class EntityCondition
{
public abstract bool RaiseEvent(EntityUid target, IEntityConditionRaiser raiser);
/// <summary>
/// If true, invert the result. So false returns true and true returns false!
/// </summary>
[DataField]
public bool Inverted;
/// <summary>
/// A basic description of this condition, which displays in the guidebook.
/// </summary>
public abstract string EntityConditionGuidebookText(IPrototypeManager prototype);
}
/// <summary>
/// An Event carrying an entity effect.
/// </summary>
/// <param name="Condition">The Condition we're checking</param>
[ByRefEvent]
public record struct EntityConditionEvent<T>(T Condition) where T : EntityConditionBase<T>
{
/// <summary>
/// The result of our check, defaults to false if nothing handles it.
/// </summary>
[DataField]
public bool Result;
/// <summary>
/// The Condition being raised in this event
/// </summary>
public readonly T Condition = Condition;
}

View File

@@ -1,23 +0,0 @@
using Robust.Shared.Prototypes;
namespace Content.Shared.EntityEffects.EffectConditions;
/// <summary>
/// Requires the target entity to be above or below a certain temperature.
/// Used for things like cryoxadone and pyroxadone.
/// </summary>
public sealed partial class Temperature : EventEntityEffectCondition<Temperature>
{
[DataField]
public float Min = 0;
[DataField]
public float Max = float.PositiveInfinity;
public override string GuidebookExplanation(IPrototypeManager prototype)
{
return Loc.GetString("reagent-effect-condition-guidebook-body-temperature",
("max", float.IsPositiveInfinity(Max) ? (float) int.MaxValue : Max),
("min", Min));
}
}

View File

@@ -1,21 +0,0 @@
using Robust.Shared.Prototypes;
namespace Content.Shared.EntityEffects.EffectConditions;
/// <summary>
/// Condition for if the entity is successfully breathing.
/// </summary>
public sealed partial class Breathing : EventEntityEffectCondition<Breathing>
{
/// <summary>
/// If true, the entity must not have trouble breathing to pass.
/// </summary>
[DataField]
public bool IsBreathing = true;
public override string GuidebookExplanation(IPrototypeManager prototype)
{
return Loc.GetString("reagent-effect-condition-guidebook-breathing",
("isBreathing", IsBreathing));
}
}

View File

@@ -1,28 +0,0 @@
using Content.Shared.Tag;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.EntityEffects.EffectConditions;
public sealed partial class HasTag : EntityEffectCondition
{
[DataField(customTypeSerializer: typeof(PrototypeIdSerializer<TagPrototype>))]
public string Tag = default!;
[DataField]
public bool Invert = false;
public override bool Condition(EntityEffectBaseArgs args)
{
if (args.EntityManager.TryGetComponent<TagComponent>(args.TargetEntity, out var tag))
return args.EntityManager.System<TagSystem>().HasTag(tag, Tag) ^ Invert;
return false;
}
public override string GuidebookExplanation(IPrototypeManager prototype)
{
// this should somehow be made (much) nicer.
return Loc.GetString("reagent-effect-condition-guidebook-has-tag", ("tag", Tag), ("invert", Invert));
}
}

View File

@@ -1,30 +0,0 @@
using Content.Shared.Body.Components;
using Robust.Shared.Prototypes;
namespace Content.Shared.EntityEffects.EffectConditions;
/// <summary>
/// Condition for if the entity is or isn't wearing internals.
/// </summary>
public sealed partial class Internals : EntityEffectCondition
{
/// <summary>
/// To pass, the entity's internals must have this same state.
/// </summary>
[DataField]
public bool UsingInternals = true;
public override bool Condition(EntityEffectBaseArgs args)
{
if (!args.EntityManager.TryGetComponent(args.TargetEntity, out InternalsComponent? internalsComp))
return !UsingInternals; // They have no internals to wear.
var internalsState = internalsComp.GasTankEntity != null; // If gas tank is not null, they are wearing internals
return UsingInternals == internalsState;
}
public override string GuidebookExplanation(IPrototypeManager prototype)
{
return Loc.GetString("reagent-effect-condition-guidebook-internals", ("usingInternals", UsingInternals));
}
}

View File

@@ -1,52 +0,0 @@
using System.Linq;
using Content.Shared.Localizations;
using Content.Shared.Mind;
using Content.Shared.Mind.Components;
using Content.Shared.Roles;
using Content.Shared.Roles.Components;
using Robust.Shared.Prototypes;
namespace Content.Shared.EntityEffects.EffectConditions;
public sealed partial class JobCondition : EntityEffectCondition
{
[DataField(required: true)] public List<ProtoId<JobPrototype>> Job;
public override bool Condition(EntityEffectBaseArgs args)
{
args.EntityManager.TryGetComponent<MindContainerComponent>(args.TargetEntity, out var mindContainer);
if (mindContainer is null
|| !args.EntityManager.TryGetComponent<MindComponent>(mindContainer.Mind, out var mind))
return false;
foreach (var roleId in mind.MindRoleContainer.ContainedEntities)
{
if (!args.EntityManager.HasComponent<JobRoleComponent>(roleId))
continue;
if (!args.EntityManager.TryGetComponent<MindRoleComponent>(roleId, out var mindRole))
{
Logger.Error($"Encountered job mind role entity {roleId} without a {nameof(MindRoleComponent)}");
continue;
}
if (mindRole.JobPrototype == null)
{
Logger.Error($"Encountered job mind role entity {roleId} without a {nameof(JobPrototype)}");
continue;
}
if (Job.Contains(mindRole.JobPrototype.Value))
return true;
}
return false;
}
public override string GuidebookExplanation(IPrototypeManager prototype)
{
var localizedNames = Job.Select(jobId => prototype.Index(jobId).LocalizedName).ToList();
return Loc.GetString("reagent-effect-condition-guidebook-job-condition", ("job", ContentLocalizationManager.FormatListToOr(localizedNames)));
}
}

View File

@@ -1,28 +0,0 @@
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Robust.Shared.Prototypes;
namespace Content.Shared.EntityEffects.EffectConditions;
public sealed partial class MobStateCondition : EntityEffectCondition
{
[DataField]
public MobState Mobstate = MobState.Alive;
public override bool Condition(EntityEffectBaseArgs args)
{
if (args.EntityManager.TryGetComponent(args.TargetEntity, out MobStateComponent? mobState))
{
if (mobState.CurrentState == Mobstate)
return true;
}
return false;
}
public override string GuidebookExplanation(IPrototypeManager prototype)
{
return Loc.GetString("reagent-effect-condition-guidebook-mob-state-condition", ("state", Mobstate));
}
}

View File

@@ -1,28 +0,0 @@
// using Content.Server.Body.Components;
using Content.Shared.Body.Prototypes;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.EntityEffects.EffectConditions;
/// <summary>
/// Requires that the metabolizing organ is or is not tagged with a certain MetabolizerType
/// </summary>
public sealed partial class OrganType : EventEntityEffectCondition<OrganType>
{
[DataField(required: true, customTypeSerializer: typeof(PrototypeIdSerializer<MetabolizerTypePrototype>))]
public string Type = default!;
/// <summary>
/// Does this condition pass when the organ has the type, or when it doesn't have the type?
/// </summary>
[DataField]
public bool ShouldHave = true;
public override string GuidebookExplanation(IPrototypeManager prototype)
{
return Loc.GetString("reagent-effect-condition-guidebook-organ-type",
("name", prototype.Index<MetabolizerTypePrototype>(Type).LocalizedName),
("shouldhave", ShouldHave));
}
}

View File

@@ -1,56 +0,0 @@
using Content.Shared.Chemistry.Reagent;
using Content.Shared.FixedPoint;
using Robust.Shared.Prototypes;
namespace Content.Shared.EntityEffects.EffectConditions;
/// <summary>
/// Used for implementing reagent effects that require a certain amount of reagent before it should be applied.
/// For instance, overdoses.
///
/// This can also trigger on -other- reagents, not just the one metabolizing. By default, it uses the
/// one being metabolized.
/// </summary>
public sealed partial class ReagentThreshold : EntityEffectCondition
{
[DataField]
public FixedPoint2 Min = FixedPoint2.Zero;
[DataField]
public FixedPoint2 Max = FixedPoint2.MaxValue;
// TODO use ReagentId
[DataField]
public string? Reagent;
public override bool Condition(EntityEffectBaseArgs args)
{
if (args is EntityEffectReagentArgs reagentArgs)
{
var reagent = Reagent ?? reagentArgs.Reagent?.ID;
if (reagent == null)
return true; // No condition to apply.
var quant = FixedPoint2.Zero;
if (reagentArgs.Source != null)
quant = reagentArgs.Source.GetTotalPrototypeQuantity(reagent);
return quant >= Min && quant <= Max;
}
// TODO: Someone needs to figure out how to do this for non-reagent effects.
throw new NotImplementedException();
}
public override string GuidebookExplanation(IPrototypeManager prototype)
{
ReagentPrototype? reagentProto = null;
if (Reagent is not null)
prototype.TryIndex(Reagent, out reagentProto);
return Loc.GetString("reagent-effect-condition-guidebook-reagent-threshold",
("reagent", reagentProto?.LocalizedName ?? Loc.GetString("reagent-effect-condition-guidebook-this-reagent")),
("max", Max == FixedPoint2.MaxValue ? (float) int.MaxValue : Max.Float()),
("min", Min.Float()));
}
}

View File

@@ -1,36 +0,0 @@
using Robust.Shared.Prototypes;
namespace Content.Shared.EntityEffects.EffectConditions;
/// <summary>
/// Requires the solution to be above or below a certain temperature.
/// Used for things like explosives.
/// </summary>
public sealed partial class SolutionTemperature : EntityEffectCondition
{
[DataField]
public float Min = 0.0f;
[DataField]
public float Max = float.PositiveInfinity;
public override bool Condition(EntityEffectBaseArgs args)
{
if (args is EntityEffectReagentArgs reagentArgs)
{
return reagentArgs?.Source != null &&
reagentArgs.Source.Temperature >= Min &&
reagentArgs.Source.Temperature <= Max;
}
// TODO: Someone needs to figure out how to do this for non-reagent effects.
throw new NotImplementedException();
}
public override string GuidebookExplanation(IPrototypeManager prototype)
{
return Loc.GetString("reagent-effect-condition-guidebook-solution-temperature",
("max", float.IsPositiveInfinity(Max) ? (float) int.MaxValue : Max),
("min", Min));
}
}

View File

@@ -1,33 +0,0 @@
using Content.Shared.EntityEffects;
using Content.Shared.Damage;
using Content.Shared.FixedPoint;
using Robust.Shared.Prototypes;
namespace Content.Shared.EntityEffects.EffectConditions;
public sealed partial class TotalDamage : EntityEffectCondition
{
[DataField]
public FixedPoint2 Max = FixedPoint2.MaxValue;
[DataField]
public FixedPoint2 Min = FixedPoint2.Zero;
public override bool Condition(EntityEffectBaseArgs args)
{
if (args.EntityManager.TryGetComponent(args.TargetEntity, out DamageableComponent? damage))
{
var total = damage.TotalDamage;
return total >= Min && total <= Max;
}
return false;
}
public override string GuidebookExplanation(IPrototypeManager prototype)
{
return Loc.GetString("reagent-effect-condition-guidebook-total-damage",
("max", Max == FixedPoint2.MaxValue ? (float) int.MaxValue : Max.Float()),
("min", Min.Float()));
}
}

View File

@@ -1,33 +0,0 @@
using Content.Shared.EntityEffects;
using Content.Shared.Nutrition.Components;
using Content.Shared.Nutrition.EntitySystems;
using Robust.Shared.Prototypes;
namespace Content.Shared.EntityEffects.EffectConditions;
public sealed partial class Hunger : EntityEffectCondition
{
[DataField]
public float Max = float.PositiveInfinity;
[DataField]
public float Min = 0;
public override bool Condition(EntityEffectBaseArgs args)
{
if (args.EntityManager.TryGetComponent(args.TargetEntity, out HungerComponent? hunger))
{
var total = args.EntityManager.System<HungerSystem>().GetHunger(hunger);
return total >= Min && total <= Max;
}
return false;
}
public override string GuidebookExplanation(IPrototypeManager prototype)
{
return Loc.GetString("reagent-effect-condition-guidebook-total-hunger",
("max", float.IsPositiveInfinity(Max) ? (float) int.MaxValue : Max),
("min", Min));
}
}

View File

@@ -1,35 +0,0 @@
using Content.Shared.Chemistry.EntitySystems;
using Robust.Shared.Prototypes;
namespace Content.Shared.EntityEffects.Effects
{
public sealed partial class AddToSolutionReaction : EntityEffect
{
[DataField("solution")]
private string _solution = "reagents";
public override void Effect(EntityEffectBaseArgs args)
{
if (args is EntityEffectReagentArgs reagentArgs) {
if (reagentArgs.Reagent == null)
return;
// TODO see if this is correct
var solutionContainerSystem = reagentArgs.EntityManager.System<SharedSolutionContainerSystem>();
if (!solutionContainerSystem.TryGetSolution(reagentArgs.TargetEntity, _solution, out var solutionContainer))
return;
if (solutionContainerSystem.TryAddReagent(solutionContainer.Value, reagentArgs.Reagent.ID, reagentArgs.Quantity, out var accepted))
reagentArgs.Source?.RemoveReagent(reagentArgs.Reagent.ID, accepted);
return;
}
// TODO: Someone needs to figure out how to do this for non-reagent effects.
throw new NotImplementedException();
}
protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) =>
Loc.GetString("reagent-effect-guidebook-add-to-solution-reaction", ("chance", Probability));
}
}

View File

@@ -1,58 +0,0 @@
using Content.Shared.Alert;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Shared.EntityEffects.Effects;
public sealed partial class AdjustAlert : EntityEffect
{
/// <summary>
/// The specific Alert that will be adjusted
/// </summary>
[DataField(required: true)]
public ProtoId<AlertPrototype> AlertType;
/// <summary>
/// If true, the alert is removed after Time seconds. If Time was not specified the alert is removed immediately.
/// </summary>
[DataField]
public bool Clear;
/// <summary>
/// Visually display cooldown progress over the alert icon.
/// </summary>
[DataField]
public bool ShowCooldown;
/// <summary>
/// The length of the cooldown or delay before removing the alert (in seconds).
/// </summary>
[DataField]
public float Time;
//JUSTIFICATION: This just changes some visuals, doesn't need to be documented.
protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => null;
public override void Effect(EntityEffectBaseArgs args)
{
var alertSys = args.EntityManager.EntitySysManager.GetEntitySystem<AlertsSystem>();
if (!args.EntityManager.HasComponent<AlertsComponent>(args.TargetEntity))
return;
if (Clear && Time <= 0)
{
alertSys.ClearAlert(args.TargetEntity, AlertType);
}
else
{
var timing = IoCManager.Resolve<IGameTiming>();
(TimeSpan, TimeSpan)? cooldown = null;
if ((ShowCooldown || Clear) && Time > 0)
cooldown = (timing.CurTime, timing.CurTime + TimeSpan.FromSeconds(Time));
alertSys.ShowAlert(args.TargetEntity, AlertType, cooldown: cooldown, autoRemove: Clear, showCooldown: ShowCooldown);
}
}
}

View File

@@ -0,0 +1,65 @@
using Content.Shared.Alert;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Shared.EntityEffects.Effects;
/// <summary>
/// Adjusts a given alert on this entity.
/// </summary>
/// <inheritdoc cref="EntityEffectSystem{T,TEffect}"/>
public sealed partial class AdjustAlertEntityEffectSysten : EntityEffectSystem<AlertsComponent, AdjustAlert>
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly AlertsSystem _alerts = default!;
protected override void Effect(Entity<AlertsComponent> entity, ref EntityEffectEvent<AdjustAlert> args)
{
var time = args.Effect.Time;
var clear = args.Effect.Clear;
var type = args.Effect.AlertType;
if (clear && time <= TimeSpan.Zero)
{
_alerts.ClearAlert(entity.AsNullable(), type);
}
else
{
(TimeSpan, TimeSpan)? cooldown = null;
if ((args.Effect.ShowCooldown || clear) && args.Effect.Time >= TimeSpan.Zero)
cooldown = (_timing.CurTime, _timing.CurTime + time);
_alerts.ShowAlert(entity.AsNullable(), type, cooldown: cooldown, autoRemove: clear, showCooldown: args.Effect.ShowCooldown);
}
}
}
/// <inheritdoc cref="EntityEffect"/>
public sealed partial class AdjustAlert : EntityEffectBase<AdjustAlert>
{
/// <summary>
/// The specific Alert that will be adjusted
/// </summary>
[DataField(required: true)]
public ProtoId<AlertPrototype> AlertType;
/// <summary>
/// If true, the alert is removed after Time seconds. If Time was not specified the alert is removed immediately.
/// </summary>
[DataField]
public bool Clear;
/// <summary>
/// Visually display cooldown progress over the alert icon.
/// </summary>
[DataField]
public bool ShowCooldown;
/// <summary>
/// The length of the cooldown or delay before removing the alert (in seconds).
/// </summary>
[DataField]
public TimeSpan Time;
}

View File

@@ -1,90 +0,0 @@
using Content.Shared.Body.Prototypes;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.FixedPoint;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.EntityEffects.Effects
{
public sealed partial class AdjustReagent : EntityEffect
{
/// <summary>
/// The reagent ID to remove. Only one of this and <see cref="Group"/> should be active.
/// </summary>
[DataField(customTypeSerializer: typeof(PrototypeIdSerializer<ReagentPrototype>))]
public string? Reagent = null;
// TODO use ReagentId
/// <summary>
/// The metabolism group to remove, if the reagent satisfies any.
/// Only one of this and <see cref="Reagent"/> should be active.
/// </summary>
[DataField(customTypeSerializer: typeof(PrototypeIdSerializer<MetabolismGroupPrototype>))]
public string? Group = null;
[DataField(required: true)]
public FixedPoint2 Amount = default!;
public override void Effect(EntityEffectBaseArgs args)
{
if (args is EntityEffectReagentArgs reagentArgs)
{
if (reagentArgs.Source == null)
return;
var amount = Amount;
amount *= reagentArgs.Scale;
if (Reagent != null)
{
if (amount < 0 && reagentArgs.Source.ContainsPrototype(Reagent))
reagentArgs.Source.RemoveReagent(Reagent, -amount);
if (amount > 0)
reagentArgs.Source.AddReagent(Reagent, amount);
}
else if (Group != null)
{
var prototypeMan = IoCManager.Resolve<IPrototypeManager>();
foreach (var quant in reagentArgs.Source.Contents.ToArray())
{
var proto = prototypeMan.Index<ReagentPrototype>(quant.Reagent.Prototype);
if (proto.Metabolisms != null && proto.Metabolisms.ContainsKey(Group))
{
if (amount < 0)
reagentArgs.Source.RemoveReagent(quant.Reagent, amount);
if (amount > 0)
reagentArgs.Source.AddReagent(quant.Reagent, amount);
}
}
}
return;
}
// TODO: Someone needs to figure out how to do this for non-reagent effects.
throw new NotImplementedException();
}
protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
{
if (Reagent is not null && prototype.TryIndex(Reagent, out ReagentPrototype? reagentProto))
{
return Loc.GetString("reagent-effect-guidebook-adjust-reagent-reagent",
("chance", Probability),
("deltasign", MathF.Sign(Amount.Float())),
("reagent", reagentProto.LocalizedName),
("amount", MathF.Abs(Amount.Float())));
}
else if (Group is not null && prototype.TryIndex(Group, out MetabolismGroupPrototype? groupProto))
{
return Loc.GetString("reagent-effect-guidebook-adjust-reagent-group",
("chance", Probability),
("deltasign", MathF.Sign(Amount.Float())),
("group", groupProto.LocalizedName),
("amount", MathF.Abs(Amount.Float())));
}
throw new NotImplementedException();
}
}
}

View File

@@ -1,15 +0,0 @@
using Robust.Shared.Prototypes;
namespace Content.Shared.EntityEffects.Effects;
public sealed partial class AdjustTemperature : EventEntityEffect<AdjustTemperature>
{
[DataField]
public float Amount;
protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
=> Loc.GetString("reagent-effect-guidebook-adjust-temperature",
("chance", Probability),
("deltasign", MathF.Sign(Amount)),
("amount", MathF.Abs(Amount)));
}

View File

@@ -0,0 +1,30 @@
using Content.Shared.Temperature.Components;
using Content.Shared.Temperature.Systems;
namespace Content.Shared.EntityEffects.Effects;
// TODO: When we get a proper temperature/energy struct combine this with the solution temperature effect!!!
/// <summary>
/// Adjusts the temperature of this entity.
/// </summary>
/// <inheritdoc cref="EntityEffectSystem{T,TEffect}"/>
public sealed partial class AdjustTemperatureEntityEffectSystem : EntityEffectSystem<TemperatureComponent, AdjustTemperature>
{
[Dependency] private readonly SharedTemperatureSystem _temperature = default!;
protected override void Effect(Entity<TemperatureComponent> entity, ref EntityEffectEvent<AdjustTemperature> args)
{
var amount = args.Effect.Amount * args.Scale;
_temperature.ChangeHeat(entity, amount, true, entity.Comp);
}
}
/// <inheritdoc cref="EntityEffect"/>
public sealed partial class AdjustTemperature : EntityEffectBase<AdjustTemperature>
{
/// <summary>
/// Amount we're adjusting temperature by.
/// </summary>
[DataField]
public float Amount;
}

View File

@@ -1,43 +0,0 @@
using Content.Shared.Database;
using Content.Shared.FixedPoint;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.EntityEffects.Effects;
/// <summary>
/// Basically smoke and foam reactions.
/// </summary>
public sealed partial class AreaReactionEffect : EventEntityEffect<AreaReactionEffect>
{
/// <summary>
/// How many seconds will the effect stay, counting after fully spreading.
/// </summary>
[DataField("duration")] public float Duration = 10;
/// <summary>
/// How many units of reaction for 1 smoke entity.
/// </summary>
[DataField] public FixedPoint2 OverflowThreshold = FixedPoint2.New(2.5);
/// <summary>
/// The entity prototype that will be spawned as the effect.
/// </summary>
[DataField("prototypeId", required: true, customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
public string PrototypeId = default!;
/// <summary>
/// Sound that will get played when this reaction effect occurs.
/// </summary>
[DataField("sound", required: true)] public SoundSpecifier Sound = default!;
public override bool ShouldLog => true;
protected override string ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
=> Loc.GetString("reagent-effect-guidebook-area-reaction",
("duration", Duration)
);
public override LogImpact LogImpact => LogImpact.High;
}

Some files were not shown because too many files have changed in this diff Show More