From 78a94730bea427661e6e147b2bd76b6de4b2512a Mon Sep 17 00:00:00 2001 From: Red <96445749+TheShuEd@users.noreply.github.com> Date: Wed, 25 Jun 2025 14:41:35 +0300 Subject: [PATCH] New status effect system (#37238) * spectra * documentation * added into liquid anomaly * Update TemporaryStealthComponent.cs * Update TemporaryStealthComponent.cs * integrated * new system * mark old status effect system as obsolete * ForcedSleeping new status effect * work with reagents * networking??? * Revert "integrated" This reverts commit bca02b82bae18ae131af593d7eb86e6de2745157. * Revert "Update TemporaryStealthComponent.cs" This reverts commit 4a5be8c4b704a0d1ff9544b2e245d8b2701ec580. * Revert "Update TemporaryStealthComponent.cs" This reverts commit a4875bcb41347638854bd723d96a51c3e6d38034. * Revert "added into liquid anomaly" This reverts commit df5086b14bb35f1467158a36807c0f2163a16d99. * Revert "documentation" This reverts commit 3629b9466758cbdfa4dd5e67ece122fa2f181138. * Revert "spectra" This reverts commit 2d03d88c16d16ad6831c19a7921b84600daeb284. * drowsiness status effect remove * reagents work * polish, remove test changes * first Fildrance review part * Update misc.yml * more fildrance review * final part * fix trailing spaces * sleeping status effect * drowsiness status effect * Create ModifyStatusEffect.cs * some tweak * Yay!!! Manual networking * minor nitpick * oopsie * refactor: xml-docs, notnullwhen attributes, whitespaces * fildrance and emo review * refactor: simplify check in SharedStatusEffectsSystem by using pattern matching, TryEffectsWithComp now returns set of Entity --------- Co-authored-by: pa.pecherskij --- .../Drowsiness/DrowsinessOverlay.cs | 25 +- Content.Client/Drowsiness/DrowsinessSystem.cs | 53 ++-- .../ClientStatusEffectsSystem.cs | 50 ++++ Content.Server/Drowsiness/DrowsinessSystem.cs | 33 ++- .../StatusEffectNew/StatusEffectsSystem.cs | 23 ++ .../Traits/Assorted/NarcolepsySystem.cs | 8 +- .../Bed/Sleep/ForcedSleepingComponent.cs | 11 - .../ForcedSleepingStatusEffectComponent.cs | 10 + Content.Shared/Bed/Sleep/SleepingSystem.cs | 17 +- .../Damage/Systems/SharedGodmodeSystem.cs | 2 +- ....cs => DrowsinessStatusEffectComponent.cs} | 14 +- Content.Shared/Drowsiness/DrowsinessSystem.cs | 4 - .../StatusEffects/GenericStatusEffect.cs | 1 + .../StatusEffects/ModifyStatusEffect.cs | 66 +++++ .../SSDIndicator/SSDIndicatorSystem.cs | 13 +- .../StatusEffect/StatusEffectsSystem.cs | 19 +- .../Components/StatusEffectComponent.cs | 47 ++++ .../StatusEffectContainerComponent.cs | 23 ++ .../SharedStatusEffectsSystem.cs | 208 ++++++++++++++ .../StatusEffectNewSystem.API.cs | 265 ++++++++++++++++++ Resources/Locale/en-US/entity-categories.ftl | 1 + .../Entities/Mobs/NPCs/asteroid.yml | 1 - .../Entities/Mobs/NPCs/simplemob.yml | 4 - .../Entities/Mobs/Player/dragon.yml | 2 - .../Prototypes/Entities/Mobs/Species/base.yml | 2 - .../Entities/StatusEffects/misc.yml | 36 +++ Resources/Prototypes/Entities/categories.yml | 5 + .../Reagents/Consumable/Drink/alcohol.yml | 36 +-- .../Reagents/Consumable/Drink/drinks.yml | 30 +- .../Reagents/Consumable/Drink/soda.yml | 14 +- Resources/Prototypes/Reagents/gases.yml | 5 +- Resources/Prototypes/Reagents/medicine.yml | 18 +- Resources/Prototypes/Reagents/narcotics.yml | 23 +- Resources/Prototypes/Reagents/toxins.yml | 5 +- Resources/Prototypes/status_effects.yml | 9 +- 35 files changed, 913 insertions(+), 170 deletions(-) create mode 100644 Content.Client/StatusEffectNew/ClientStatusEffectsSystem.cs create mode 100644 Content.Server/StatusEffectNew/StatusEffectsSystem.cs delete mode 100644 Content.Shared/Bed/Sleep/ForcedSleepingComponent.cs create mode 100644 Content.Shared/Bed/Sleep/ForcedSleepingStatusEffectComponent.cs rename Content.Shared/Drowsiness/{DrowsinessComponent.cs => DrowsinessStatusEffectComponent.cs} (56%) create mode 100644 Content.Shared/EntityEffects/Effects/StatusEffects/ModifyStatusEffect.cs create mode 100644 Content.Shared/StatusEffectNew/Components/StatusEffectComponent.cs create mode 100644 Content.Shared/StatusEffectNew/Components/StatusEffectContainerComponent.cs create mode 100644 Content.Shared/StatusEffectNew/SharedStatusEffectsSystem.cs create mode 100644 Content.Shared/StatusEffectNew/StatusEffectNewSystem.API.cs create mode 100644 Resources/Prototypes/Entities/StatusEffects/misc.yml diff --git a/Content.Client/Drowsiness/DrowsinessOverlay.cs b/Content.Client/Drowsiness/DrowsinessOverlay.cs index a316f31ae6..9705aa7313 100644 --- a/Content.Client/Drowsiness/DrowsinessOverlay.cs +++ b/Content.Client/Drowsiness/DrowsinessOverlay.cs @@ -1,5 +1,7 @@ +using Content.Shared.Bed.Sleep; using Content.Shared.Drowsiness; -using Content.Shared.StatusEffect; +using Content.Shared.StatusEffectNew; +using Content.Shared.StatusEffectNew.Components; using Robust.Client.Graphics; using Robust.Client.Player; using Robust.Shared.Enums; @@ -15,11 +17,14 @@ public sealed class DrowsinessOverlay : Overlay [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IEntitySystemManager _sysMan = default!; [Dependency] private readonly IGameTiming _timing = default!; + private readonly SharedStatusEffectsSystem _statusEffects = default!; public override OverlaySpace Space => OverlaySpace.WorldSpace; public override bool RequestScreenTexture => true; private readonly ShaderInstance _drowsinessShader; + private EntityQuery _statusQuery; + public float CurrentPower = 0.0f; private const float PowerDivisor = 250.0f; @@ -29,6 +34,9 @@ public sealed class DrowsinessOverlay : Overlay public DrowsinessOverlay() { IoCManager.InjectDependencies(this); + _statusEffects = _sysMan.GetEntitySystem(); + + _statusQuery = _entityManager.GetEntityQuery(); _drowsinessShader = _prototypeManager.Index("Drowsiness").InstanceUnique(); } @@ -39,16 +47,21 @@ public sealed class DrowsinessOverlay : Overlay if (playerEntity == null) return; - if (!_entityManager.HasComponent(playerEntity) - || !_entityManager.TryGetComponent(playerEntity, out var status)) + if (!_statusEffects.TryEffectsWithComp(playerEntity, out var drowsinessEffects)) return; - var statusSys = _sysMan.GetEntitySystem(); - if (!statusSys.TryGetTime(playerEntity.Value, SharedDrowsinessSystem.DrowsinessKey, out var time, status)) + TimeSpan? remainingTime = TimeSpan.Zero; + foreach (var (_, _, statusEffectComp) in drowsinessEffects) + { + if (statusEffectComp.EndEffectTime > remainingTime) + remainingTime = statusEffectComp.EndEffectTime; + } + + if (remainingTime is null) return; var curTime = _timing.CurTime; - var timeLeft = (float)(time.Value.Item2 - curTime).TotalSeconds; + var timeLeft = (float)(remainingTime - curTime).Value.TotalSeconds; CurrentPower += 8f * (0.5f * timeLeft - CurrentPower) * args.DeltaSeconds / (timeLeft + 1); } diff --git a/Content.Client/Drowsiness/DrowsinessSystem.cs b/Content.Client/Drowsiness/DrowsinessSystem.cs index bc8862b19d..152d6ebdf8 100644 --- a/Content.Client/Drowsiness/DrowsinessSystem.cs +++ b/Content.Client/Drowsiness/DrowsinessSystem.cs @@ -1,7 +1,7 @@ using Content.Shared.Drowsiness; +using Content.Shared.StatusEffectNew; using Robust.Client.Graphics; using Robust.Client.Player; -using Robust.Shared.Player; namespace Content.Client.Drowsiness; @@ -9,6 +9,7 @@ public sealed class DrowsinessSystem : SharedDrowsinessSystem { [Dependency] private readonly IPlayerManager _player = default!; [Dependency] private readonly IOverlayManager _overlayMan = default!; + [Dependency] private readonly SharedStatusEffectsSystem _statusEffects = default!; private DrowsinessOverlay _overlay = default!; @@ -16,35 +17,47 @@ public sealed class DrowsinessSystem : SharedDrowsinessSystem { base.Initialize(); - SubscribeLocalEvent(OnDrowsinessInit); - SubscribeLocalEvent(OnDrowsinessShutdown); + SubscribeLocalEvent(OnDrowsinessApply); + SubscribeLocalEvent(OnDrowsinessShutdown); - SubscribeLocalEvent(OnPlayerAttached); - SubscribeLocalEvent(OnPlayerDetached); + SubscribeLocalEvent(OnStatusEffectPlayerAttached); + SubscribeLocalEvent(OnStatusEffectPlayerDetached); _overlay = new(); } - private void OnPlayerAttached(EntityUid uid, DrowsinessComponent component, LocalPlayerAttachedEvent args) + private void OnDrowsinessApply(Entity ent, ref StatusEffectAppliedEvent args) { - _overlayMan.AddOverlay(_overlay); - } - - private void OnPlayerDetached(EntityUid uid, DrowsinessComponent component, LocalPlayerDetachedEvent args) - { - _overlay.CurrentPower = 0; - _overlayMan.RemoveOverlay(_overlay); - } - - private void OnDrowsinessInit(EntityUid uid, DrowsinessComponent component, ComponentInit args) - { - if (_player.LocalEntity == uid) + if (_player.LocalEntity == args.Target) _overlayMan.AddOverlay(_overlay); } - private void OnDrowsinessShutdown(EntityUid uid, DrowsinessComponent component, ComponentShutdown args) + private void OnDrowsinessShutdown(Entity ent, ref StatusEffectRemovedEvent args) { - if (_player.LocalEntity == uid) + if (_player.LocalEntity != args.Target) + return; + + if (!_statusEffects.HasEffectComp(_player.LocalEntity.Value)) + { + _overlay.CurrentPower = 0; + _overlayMan.RemoveOverlay(_overlay); + } + } + + private void OnStatusEffectPlayerAttached(Entity ent, ref StatusEffectPlayerAttachedEvent args) + { + if (_player.LocalEntity != args.Target) + return; + + _overlayMan.AddOverlay(_overlay); + } + + private void OnStatusEffectPlayerDetached(Entity ent, ref StatusEffectPlayerDetachedEvent args) + { + if (_player.LocalEntity != args.Target) + return; + + if (!_statusEffects.HasEffectComp(_player.LocalEntity.Value)) { _overlay.CurrentPower = 0; _overlayMan.RemoveOverlay(_overlay); diff --git a/Content.Client/StatusEffectNew/ClientStatusEffectsSystem.cs b/Content.Client/StatusEffectNew/ClientStatusEffectsSystem.cs new file mode 100644 index 0000000000..e35c09190e --- /dev/null +++ b/Content.Client/StatusEffectNew/ClientStatusEffectsSystem.cs @@ -0,0 +1,50 @@ +using Content.Shared.StatusEffectNew; +using Content.Shared.StatusEffectNew.Components; +using Robust.Shared.Collections; +using Robust.Shared.GameStates; + +namespace Content.Client.StatusEffectNew; + +/// +public sealed partial class ClientStatusEffectsSystem : SharedStatusEffectsSystem +{ + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnHandleState); + } + + private void OnHandleState(Entity ent, ref ComponentHandleState args) + { + if (args.Current is not StatusEffectContainerComponentState state) + return; + + var toRemove = new ValueList(); + foreach (var effect in ent.Comp.ActiveStatusEffects) + { + if (state.ActiveStatusEffects.Contains(GetNetEntity(effect))) + continue; + + toRemove.Add(effect); + } + + foreach (var effect in toRemove) + { + ent.Comp.ActiveStatusEffects.Remove(effect); + var ev = new StatusEffectRemovedEvent(ent); + RaiseLocalEvent(effect, ref ev); + } + + foreach (var effect in state.ActiveStatusEffects) + { + var effectUid = GetEntity(effect); + if (ent.Comp.ActiveStatusEffects.Contains(effectUid)) + continue; + + ent.Comp.ActiveStatusEffects.Add(effectUid); + var ev = new StatusEffectAppliedEvent(ent); + RaiseLocalEvent(effectUid, ref ev); + } + } +} diff --git a/Content.Server/Drowsiness/DrowsinessSystem.cs b/Content.Server/Drowsiness/DrowsinessSystem.cs index 2511bc790e..0489b16f51 100644 --- a/Content.Server/Drowsiness/DrowsinessSystem.cs +++ b/Content.Server/Drowsiness/DrowsinessSystem.cs @@ -1,6 +1,8 @@ -using Content.Shared.Bed.Sleep; +using Content.Server.StatusEffectNew; +using Content.Shared.Bed.Sleep; using Content.Shared.Drowsiness; -using Content.Shared.StatusEffect; +using Content.Shared.StatusEffectNew; +using Content.Shared.StatusEffectNew.Components; using Robust.Shared.Random; using Robust.Shared.Timing; @@ -8,9 +10,6 @@ namespace Content.Server.Drowsiness; public sealed class DrowsinessSystem : SharedDrowsinessSystem { - [ValidatePrototypeId] - private const string SleepKey = "ForcedSleep"; // Same one used by N2O and other sleep chems. - [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly StatusEffectsSystem _statusEffects = default!; @@ -18,33 +17,37 @@ public sealed class DrowsinessSystem : SharedDrowsinessSystem /// public override void Initialize() { - SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnEffectApplied); } - private void OnInit(EntityUid uid, DrowsinessComponent component, ComponentStartup args) + private void OnEffectApplied(Entity ent, ref StatusEffectAppliedEvent args) { - component.NextIncidentTime = _timing.CurTime + TimeSpan.FromSeconds(_random.NextFloat(component.TimeBetweenIncidents.X, component.TimeBetweenIncidents.Y)); + ent.Comp.NextIncidentTime = _timing.CurTime + TimeSpan.FromSeconds(_random.NextFloat(ent.Comp.TimeBetweenIncidents.X, ent.Comp.TimeBetweenIncidents.Y)); } + public override void Update(float frameTime) { base.Update(frameTime); - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var component)) + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var drowsiness, out var statusEffect)) { - if (_timing.CurTime < component.NextIncidentTime) + if (_timing.CurTime < drowsiness.NextIncidentTime) + continue; + + if (statusEffect.AppliedTo is null) continue; // Set the new time. - component.NextIncidentTime = _timing.CurTime + TimeSpan.FromSeconds(_random.NextFloat(component.TimeBetweenIncidents.X, component.TimeBetweenIncidents.Y)); + drowsiness.NextIncidentTime = _timing.CurTime + TimeSpan.FromSeconds(_random.NextFloat(drowsiness.TimeBetweenIncidents.X, drowsiness.TimeBetweenIncidents.Y)); // sleep duration - var duration = TimeSpan.FromSeconds(_random.NextFloat(component.DurationOfIncident.X, component.DurationOfIncident.Y)); + var duration = TimeSpan.FromSeconds(_random.NextFloat(drowsiness.DurationOfIncident.X, drowsiness.DurationOfIncident.Y)); // Make sure the sleep time doesn't cut into the time to next incident. - component.NextIncidentTime += duration; + drowsiness.NextIncidentTime += duration; - _statusEffects.TryAddStatusEffect(uid, SleepKey, duration, false); + _statusEffects.TryAddStatusEffect(statusEffect.AppliedTo.Value, SleepingSystem.StatusEffectForcedSleeping, duration); } } } diff --git a/Content.Server/StatusEffectNew/StatusEffectsSystem.cs b/Content.Server/StatusEffectNew/StatusEffectsSystem.cs new file mode 100644 index 0000000000..e5d7433396 --- /dev/null +++ b/Content.Server/StatusEffectNew/StatusEffectsSystem.cs @@ -0,0 +1,23 @@ +using Content.Shared.StatusEffectNew; +using Content.Shared.StatusEffectNew.Components; + +namespace Content.Server.StatusEffectNew; + +/// +public sealed partial class StatusEffectsSystem : SharedStatusEffectsSystem +{ + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnContainerShutdown); + } + + private void OnContainerShutdown(Entity ent, ref ComponentShutdown args) + { + foreach (var effect in ent.Comp.ActiveStatusEffects) + { + QueueDel(effect); + } + } +} diff --git a/Content.Server/Traits/Assorted/NarcolepsySystem.cs b/Content.Server/Traits/Assorted/NarcolepsySystem.cs index e4fa1ccbc7..9d0ff9470a 100644 --- a/Content.Server/Traits/Assorted/NarcolepsySystem.cs +++ b/Content.Server/Traits/Assorted/NarcolepsySystem.cs @@ -1,5 +1,5 @@ +using Content.Server.StatusEffectNew; using Content.Shared.Bed.Sleep; -using Content.Shared.StatusEffect; using Robust.Shared.Random; namespace Content.Server.Traits.Assorted; @@ -9,9 +9,6 @@ namespace Content.Server.Traits.Assorted; /// public sealed class NarcolepsySystem : EntitySystem { - [ValidatePrototypeId] - private const string StatusEffectKey = "ForcedSleep"; // Same one used by N2O and other sleep chems. - [Dependency] private readonly StatusEffectsSystem _statusEffects = default!; [Dependency] private readonly IRobustRandom _random = default!; @@ -56,8 +53,7 @@ public sealed class NarcolepsySystem : EntitySystem // Make sure the sleep time doesn't cut into the time to next incident. narcolepsy.NextIncidentTime += duration; - _statusEffects.TryAddStatusEffect(uid, StatusEffectKey, - TimeSpan.FromSeconds(duration), false); + _statusEffects.TryAddStatusEffect(uid, SleepingSystem.StatusEffectForcedSleeping, TimeSpan.FromSeconds(duration)); } } } diff --git a/Content.Shared/Bed/Sleep/ForcedSleepingComponent.cs b/Content.Shared/Bed/Sleep/ForcedSleepingComponent.cs deleted file mode 100644 index 197e8cc56a..0000000000 --- a/Content.Shared/Bed/Sleep/ForcedSleepingComponent.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Robust.Shared.GameStates; - -namespace Content.Shared.Bed.Sleep -{ - /// - /// Prevents waking up. Use as a status effect. - /// - [NetworkedComponent, RegisterComponent] - public sealed partial class ForcedSleepingComponent : Component - {} -} diff --git a/Content.Shared/Bed/Sleep/ForcedSleepingStatusEffectComponent.cs b/Content.Shared/Bed/Sleep/ForcedSleepingStatusEffectComponent.cs new file mode 100644 index 0000000000..b94c1d31c8 --- /dev/null +++ b/Content.Shared/Bed/Sleep/ForcedSleepingStatusEffectComponent.cs @@ -0,0 +1,10 @@ +using Content.Shared.StatusEffectNew.Components; +using Robust.Shared.GameStates; + +namespace Content.Shared.Bed.Sleep; + +/// +/// Prevents waking up. Use only in conjunction with , on the status effect entity. +/// +[NetworkedComponent, RegisterComponent] +public sealed partial class ForcedSleepingStatusEffectComponent : Component; diff --git a/Content.Shared/Bed/Sleep/SleepingSystem.cs b/Content.Shared/Bed/Sleep/SleepingSystem.cs index d1ca138431..9e1b27cfc9 100644 --- a/Content.Shared/Bed/Sleep/SleepingSystem.cs +++ b/Content.Shared/Bed/Sleep/SleepingSystem.cs @@ -19,6 +19,7 @@ using Content.Shared.Sound; using Content.Shared.Sound.Components; using Content.Shared.Speech; using Content.Shared.StatusEffect; +using Content.Shared.StatusEffectNew; using Content.Shared.Stunnable; using Content.Shared.Traits.Assorted; using Content.Shared.Verbs; @@ -37,10 +38,12 @@ public sealed partial class SleepingSystem : EntitySystem [Dependency] private readonly SharedPopupSystem _popupSystem = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedEmitSoundSystem _emitSound = default!; - [Dependency] private readonly StatusEffectsSystem _statusEffectsSystem = default!; + [Dependency] private readonly StatusEffectsSystem _statusEffectOld = default!; + [Dependency] private readonly SharedStatusEffectsSystem _statusEffectNew = default!; public static readonly EntProtoId SleepActionId = "ActionSleep"; public static readonly EntProtoId WakeActionId = "ActionWake"; + public static readonly EntProtoId StatusEffectForcedSleeping = "StatusEffectForcedSleeping"; public override void Initialize() { @@ -65,7 +68,7 @@ public sealed partial class SleepingSystem : EntitySystem SubscribeLocalEvent>(AddWakeVerb); SubscribeLocalEvent(OnInteractHand); - SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnStatusEffectApplied); SubscribeLocalEvent(OnUnbuckleAttempt); SubscribeLocalEvent(OnEmoteAttempt); @@ -104,8 +107,8 @@ public sealed partial class SleepingSystem : EntitySystem if (args.FellAsleep) { // Expiring status effects would remove the components needed for sleeping - _statusEffectsSystem.TryRemoveStatusEffect(ent.Owner, "Stun"); - _statusEffectsSystem.TryRemoveStatusEffect(ent.Owner, "KnockedDown"); + _statusEffectOld.TryRemoveStatusEffect(ent.Owner, "Stun"); + _statusEffectOld.TryRemoveStatusEffect(ent.Owner, "KnockedDown"); EnsureComp(ent); EnsureComp(ent); @@ -248,9 +251,9 @@ public sealed partial class SleepingSystem : EntitySystem _emitSound.SetEnabled((ent, spam), args.NewMobState == MobState.Alive); } - private void OnInit(Entity ent, ref ComponentInit args) + private void OnStatusEffectApplied(Entity ent, ref StatusEffectAppliedEvent args) { - TrySleeping(ent.Owner); + TrySleeping(args.Target); } private void Wake(Entity ent) @@ -307,7 +310,7 @@ public sealed partial class SleepingSystem : EntitySystem if (!Resolve(ent, ref ent.Comp, false)) return false; - if (!force && HasComp(ent)) + if (!force && _statusEffectNew.HasEffectComp(ent)) { if (user != null) { diff --git a/Content.Shared/Damage/Systems/SharedGodmodeSystem.cs b/Content.Shared/Damage/Systems/SharedGodmodeSystem.cs index b6b487430b..d10600ab56 100644 --- a/Content.Shared/Damage/Systems/SharedGodmodeSystem.cs +++ b/Content.Shared/Damage/Systems/SharedGodmodeSystem.cs @@ -3,7 +3,7 @@ using Content.Shared.Damage.Events; using Content.Shared.Destructible; using Content.Shared.Rejuvenate; using Content.Shared.Slippery; -using Content.Shared.StatusEffect; +using Content.Shared.StatusEffectNew; namespace Content.Shared.Damage.Systems; diff --git a/Content.Shared/Drowsiness/DrowsinessComponent.cs b/Content.Shared/Drowsiness/DrowsinessStatusEffectComponent.cs similarity index 56% rename from Content.Shared/Drowsiness/DrowsinessComponent.cs rename to Content.Shared/Drowsiness/DrowsinessStatusEffectComponent.cs index 7e170ed232..e509ed18cc 100644 --- a/Content.Shared/Drowsiness/DrowsinessComponent.cs +++ b/Content.Shared/Drowsiness/DrowsinessStatusEffectComponent.cs @@ -1,26 +1,28 @@ using System.Numerics; +using Content.Shared.StatusEffectNew.Components; using Robust.Shared.GameStates; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; namespace Content.Shared.Drowsiness; /// -/// Exists for use as a status effect. Adds a shader to the client that scales with the effect duration. +/// Exists for use as a status effect. Adds a shader to the client that scales with the effect duration. +/// Use only in conjunction with , on the status effect entity. /// [RegisterComponent, NetworkedComponent, AutoGenerateComponentPause] -public sealed partial class DrowsinessComponent : Component +public sealed partial class DrowsinessStatusEffectComponent : Component { /// /// The random time between sleeping incidents, (min, max). /// - [DataField(required: true)] - public Vector2 TimeBetweenIncidents = new Vector2(5f, 60f); + [DataField] + public Vector2 TimeBetweenIncidents = new(5f, 60f); /// /// The duration of sleeping incidents, (min, max). /// - [DataField(required: true)] - public Vector2 DurationOfIncident = new Vector2(2, 5); + [DataField] + public Vector2 DurationOfIncident = new(2, 5); [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] [AutoPausedField] diff --git a/Content.Shared/Drowsiness/DrowsinessSystem.cs b/Content.Shared/Drowsiness/DrowsinessSystem.cs index 97d7c0952d..d1e84f5ff6 100644 --- a/Content.Shared/Drowsiness/DrowsinessSystem.cs +++ b/Content.Shared/Drowsiness/DrowsinessSystem.cs @@ -1,9 +1,5 @@ -using Content.Shared.StatusEffect; - namespace Content.Shared.Drowsiness; public abstract class SharedDrowsinessSystem : EntitySystem { - [ValidatePrototypeId] - public const string DrowsinessKey = "Drowsiness"; } diff --git a/Content.Shared/EntityEffects/Effects/StatusEffects/GenericStatusEffect.cs b/Content.Shared/EntityEffects/Effects/StatusEffects/GenericStatusEffect.cs index c6b162a82f..b770023604 100644 --- a/Content.Shared/EntityEffects/Effects/StatusEffects/GenericStatusEffect.cs +++ b/Content.Shared/EntityEffects/Effects/StatusEffects/GenericStatusEffect.cs @@ -13,6 +13,7 @@ namespace Content.Shared.EntityEffects.Effects.StatusEffects; /// /// Can be used for things like adding accents or something. I don't know. Go wild. /// +[Obsolete("Use ModifyStatusEffect with StatusEffectNewSystem instead")] public sealed partial class GenericStatusEffect : EntityEffect { [DataField(required: true)] diff --git a/Content.Shared/EntityEffects/Effects/StatusEffects/ModifyStatusEffect.cs b/Content.Shared/EntityEffects/Effects/StatusEffects/ModifyStatusEffect.cs new file mode 100644 index 0000000000..33021ecdab --- /dev/null +++ b/Content.Shared/EntityEffects/Effects/StatusEffects/ModifyStatusEffect.cs @@ -0,0 +1,66 @@ +using Content.Shared.StatusEffectNew; +using JetBrains.Annotations; +using Robust.Shared.Prototypes; + +namespace Content.Shared.EntityEffects.Effects.StatusEffects; + +/// +/// Changes status effects on entities: Adds, removes or sets time. +/// +[UsedImplicitly] +public sealed partial class ModifyStatusEffect : EntityEffect +{ + [DataField(required: true)] + public EntProtoId EffectProto; + + /// + /// Time for which status effect should be applied. Behaviour changes according to . + /// + [DataField] + public float Time = 2.0f; + + /// + /// true - refresh status effect time, false - accumulate status effect time. + /// + [DataField] + public bool Refresh = true; + + /// + /// Should this effect add the status effect, remove time from it, or set its cooldown? + /// + [DataField] + public StatusEffectMetabolismType Type = StatusEffectMetabolismType.Add; + + /// + public override void Effect(EntityEffectBaseArgs args) + { + var statusSys = args.EntityManager.EntitySysManager.GetEntitySystem(); + + var time = Time; + if (args is EntityEffectReagentArgs reagentArgs) + time *= reagentArgs.Scale.Float(); + + switch (Type) + { + case StatusEffectMetabolismType.Add: + statusSys.TryAddStatusEffect(args.TargetEntity, EffectProto, TimeSpan.FromSeconds(time), Refresh); + break; + case StatusEffectMetabolismType.Remove: + statusSys.TryAddTime(args.TargetEntity, EffectProto, -TimeSpan.FromSeconds(time)); + break; + case StatusEffectMetabolismType.Set: + statusSys.TrySetTime(args.TargetEntity, EffectProto, TimeSpan.FromSeconds(time)); + break; + } + } + + /// + protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString( + "reagent-effect-guidebook-status-effect", + ("chance", Probability), + ("type", Type), + ("time", Time), + ("key", prototype.Index(EffectProto).Name) + ); +} diff --git a/Content.Shared/SSDIndicator/SSDIndicatorSystem.cs b/Content.Shared/SSDIndicator/SSDIndicatorSystem.cs index dfba833bcf..850ea8e8da 100644 --- a/Content.Shared/SSDIndicator/SSDIndicatorSystem.cs +++ b/Content.Shared/SSDIndicator/SSDIndicatorSystem.cs @@ -1,5 +1,6 @@ using Content.Shared.Bed.Sleep; using Content.Shared.CCVar; +using Content.Shared.StatusEffectNew; using Robust.Shared.Configuration; using Robust.Shared.Player; using Robust.Shared.Timing; @@ -13,6 +14,7 @@ public sealed class SSDIndicatorSystem : EntitySystem { [Dependency] private readonly IConfigurationManager _cfg = default!; [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly SharedStatusEffectsSystem _statusEffects = default!; private bool _icSsdSleep; private float _icSsdSleepTime; @@ -37,10 +39,11 @@ public sealed class SSDIndicatorSystem : EntitySystem component.FallAsleepTime = TimeSpan.Zero; if (component.ForcedSleepAdded) // Remove component only if it has been added by this system { - EntityManager.RemoveComponent(uid); + _statusEffects.TryRemoveStatusEffect(uid, SleepingSystem.StatusEffectForcedSleeping); component.ForcedSleepAdded = false; } } + Dirty(uid, component); } @@ -53,6 +56,7 @@ public sealed class SSDIndicatorSystem : EntitySystem { component.FallAsleepTime = _timing.CurTime + TimeSpan.FromSeconds(_icSsdSleepTime); } + Dirty(uid, component); } @@ -79,12 +83,11 @@ public sealed class SSDIndicatorSystem : EntitySystem while (query.MoveNext(out var uid, out var ssd)) { // Forces the entity to sleep when the time has come - if(ssd.IsSSD && + if (ssd.IsSSD && ssd.FallAsleepTime <= _timing.CurTime && - !TerminatingOrDeleted(uid) && - !HasComp(uid)) // Don't add the component if the entity has it from another sources + !TerminatingOrDeleted(uid)) { - EnsureComp(uid); + _statusEffects.TryAddStatusEffect(uid, SleepingSystem.StatusEffectForcedSleeping); ssd.ForcedSleepAdded = true; } } diff --git a/Content.Shared/StatusEffect/StatusEffectsSystem.cs b/Content.Shared/StatusEffect/StatusEffectsSystem.cs index 5fa634351d..9a73f83d34 100644 --- a/Content.Shared/StatusEffect/StatusEffectsSystem.cs +++ b/Content.Shared/StatusEffect/StatusEffectsSystem.cs @@ -1,6 +1,7 @@ using System.Diagnostics.CodeAnalysis; using Content.Shared.Alert; using Content.Shared.Rejuvenate; +using Content.Shared.StatusEffectNew; using Robust.Shared.GameStates; using Robust.Shared.Prototypes; using Robust.Shared.Timing; @@ -8,6 +9,7 @@ using Robust.Shared.Utility; namespace Content.Shared.StatusEffect { + [Obsolete("Migration to Content.Shared.StatusEffectNew.SharedStatusEffectsSystem is required")] public sealed class StatusEffectsSystem : EntitySystem { [Dependency] private readonly IPrototypeManager _prototypeManager = default!; @@ -104,6 +106,7 @@ namespace Content.Shared.StatusEffect /// The status effects component to change, if you already have it. /// False if the effect could not be added or the component already exists, true otherwise. /// The component type to add and remove from the entity. + [Obsolete("Migration to Content.Shared.StatusEffectNew.SharedStatusEffectsSystem is required")] public bool TryAddStatusEffect(EntityUid uid, string key, TimeSpan time, bool refresh, StatusEffectsComponent? status = null) where T : IComponent, new() @@ -123,6 +126,7 @@ namespace Content.Shared.StatusEffect } + [Obsolete("Migration to Content.Shared.StatusEffectNew.SharedStatusEffectsSystem is required")] public bool TryAddStatusEffect(EntityUid uid, string key, TimeSpan time, bool refresh, string component, StatusEffectsComponent? status = null) { @@ -162,6 +166,7 @@ namespace Content.Shared.StatusEffect /// If the effect already exists, it will simply replace the cooldown with the new one given. /// If you want special 'effect merging' behavior, do it your own damn self! /// + [Obsolete("Migration to Content.Shared.StatusEffectNew.SharedStatusEffectsSystem is required")] public bool TryAddStatusEffect(EntityUid uid, string key, TimeSpan time, @@ -255,6 +260,7 @@ namespace Content.Shared.StatusEffect /// Obviously this doesn't automatically clear any effects a status effect might have. /// That's up to the removed component to handle itself when it's removed. /// + [Obsolete("Migration to Content.Shared.StatusEffectNew.SharedStatusEffectsSystem is required")] public bool TryRemoveStatusEffect(EntityUid uid, string key, StatusEffectsComponent? status = null, bool remComp = true) { @@ -298,6 +304,7 @@ namespace Content.Shared.StatusEffect /// The entity to remove effects from. /// The status effects component to change, if you already have it. /// False if any status effects failed to be removed, true if they all did. + [Obsolete("Migration to Content.Shared.StatusEffectNew.SharedStatusEffectsSystem is required")] public bool TryRemoveAllStatusEffects(EntityUid uid, StatusEffectsComponent? status = null) { @@ -321,6 +328,7 @@ namespace Content.Shared.StatusEffect /// The entity to check on. /// The status effect ID to check for /// The status effect component, should you already have it. + [Obsolete("Migration to Content.Shared.StatusEffectNew.SharedStatusEffectsSystem is required")] public bool HasStatusEffect(EntityUid uid, string key, StatusEffectsComponent? status = null) { @@ -338,6 +346,7 @@ namespace Content.Shared.StatusEffect /// The entity to check on. /// The status effect ID to check for /// The status effect component, should you already have it. + [Obsolete("Migration to Content.Shared.StatusEffectNew.SharedStatusEffectsSystem is required")] public bool CanApplyEffect(EntityUid uid, string key, StatusEffectsComponent? status = null) { // don't log since stuff calling this prolly doesn't care if we don't actually have it @@ -364,6 +373,7 @@ namespace Content.Shared.StatusEffect /// The status effect to add time to. /// The amount of time to add. /// The status effect component, should you already have it. + [Obsolete("Migration to Content.Shared.StatusEffectNew.SharedStatusEffectsSystem is required")] public bool TryAddTime(EntityUid uid, string key, TimeSpan time, StatusEffectsComponent? status = null) { @@ -395,6 +405,7 @@ namespace Content.Shared.StatusEffect /// The status effect to remove time from. /// The amount of time to add. /// The status effect component, should you already have it. + [Obsolete("Migration to Content.Shared.StatusEffectNew.SharedStatusEffectsSystem is required")] public bool TryRemoveTime(EntityUid uid, string key, TimeSpan time, StatusEffectsComponent? status = null) { @@ -430,6 +441,7 @@ namespace Content.Shared.StatusEffect /// /// Not used internally; just sets it itself. /// + [Obsolete("Migration to Content.Shared.StatusEffectNew.SharedStatusEffectsSystem is required")] public bool TrySetTime(EntityUid uid, string key, TimeSpan time, StatusEffectsComponent? status = null) { @@ -453,6 +465,7 @@ namespace Content.Shared.StatusEffect /// Out var for the time, if it exists. /// The status effects component to use, if any. /// False if the status effect was not active, true otherwise. + [Obsolete("Migration to Content.Shared.StatusEffectNew.SharedStatusEffectsSystem is required")] public bool TryGetTime(EntityUid uid, string key, [NotNullWhen(true)] out (TimeSpan, TimeSpan)? time, StatusEffectsComponent? status = null) @@ -468,12 +481,6 @@ namespace Content.Shared.StatusEffect } } - /// - /// Raised on an entity before a status effect is added to determine if adding it should be cancelled. - /// - [ByRefEvent] - public record struct BeforeStatusEffectAddedEvent(string Key, bool Cancelled=false); - public readonly struct StatusEffectAddedEvent { public readonly EntityUid Uid; diff --git a/Content.Shared/StatusEffectNew/Components/StatusEffectComponent.cs b/Content.Shared/StatusEffectNew/Components/StatusEffectComponent.cs new file mode 100644 index 0000000000..6419874212 --- /dev/null +++ b/Content.Shared/StatusEffectNew/Components/StatusEffectComponent.cs @@ -0,0 +1,47 @@ +using Content.Shared.Alert; +using Content.Shared.Whitelist; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Shared.StatusEffectNew.Components; + +/// +/// Marker component for all status effects - every status effect entity should have it. +/// Provides a link between the effect and the affected entity, and some data common to all status effects. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause] +[Access(typeof(SharedStatusEffectsSystem))] +[EntityCategory("StatusEffects")] +public sealed partial class StatusEffectComponent : Component +{ + /// + /// The entity that this status effect is applied to. + /// + [DataField, AutoNetworkedField] + public EntityUid? AppliedTo; + + /// + /// Status effect indication for the player. If Null, no Alert will be displayed. + /// + [DataField] + public ProtoId? Alert; + + /// + /// When this effect will end. If Null, the effect lasts indefinitely. + /// + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField, AutoNetworkedField] + public TimeSpan? EndEffectTime; + + /// + /// Whitelist, by which it is determined whether this status effect can be imposed on a particular entity. + /// + [DataField] + public EntityWhitelist? Whitelist; + + /// + /// Blacklist, by which it is determined whether this status effect can be imposed on a particular entity. + /// + [DataField] + public EntityWhitelist? Blacklist; +} diff --git a/Content.Shared/StatusEffectNew/Components/StatusEffectContainerComponent.cs b/Content.Shared/StatusEffectNew/Components/StatusEffectContainerComponent.cs new file mode 100644 index 0000000000..6d9efaf3ac --- /dev/null +++ b/Content.Shared/StatusEffectNew/Components/StatusEffectContainerComponent.cs @@ -0,0 +1,23 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Serialization; + +namespace Content.Shared.StatusEffectNew.Components; + +/// +/// Adds container for status effect entities that are applied to entity. +/// Is applied automatically upon adding any status effect. +/// Can be used for tracking currently applied status effects. +/// +[RegisterComponent, NetworkedComponent] +[Access(typeof(SharedStatusEffectsSystem))] +public sealed partial class StatusEffectContainerComponent : Component +{ + [DataField] + public HashSet ActiveStatusEffects = new(); +} + +[Serializable, NetSerializable] +public sealed class StatusEffectContainerComponentState(HashSet activeStatusEffects) : ComponentState +{ + public readonly HashSet ActiveStatusEffects = activeStatusEffects; +} diff --git a/Content.Shared/StatusEffectNew/SharedStatusEffectsSystem.cs b/Content.Shared/StatusEffectNew/SharedStatusEffectsSystem.cs new file mode 100644 index 0000000000..df27dca27c --- /dev/null +++ b/Content.Shared/StatusEffectNew/SharedStatusEffectsSystem.cs @@ -0,0 +1,208 @@ +using Content.Shared.Alert; +using Content.Shared.StatusEffectNew.Components; +using Content.Shared.Whitelist; +using Robust.Shared.GameStates; +using Robust.Shared.Network; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; + +namespace Content.Shared.StatusEffectNew; + +/// +/// This system controls status effects, their lifetime, and provides an API for adding them to entities, +/// removing them from entities, or getting information about current effects on entities. +/// +public abstract partial class SharedStatusEffectsSystem : EntitySystem +{ + [Dependency] private readonly AlertsSystem _alerts = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelist = default!; + [Dependency] private readonly IPrototypeManager _proto = default!; + [Dependency] private readonly IComponentFactory _compFactory = default!; + [Dependency] private readonly INetManager _net = default!; + + private EntityQuery _containerQuery; + private EntityQuery _effectQuery; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnStatusEffectApplied); + SubscribeLocalEvent(OnStatusEffectRemoved); + + SubscribeLocalEvent(OnStatusEffectContainerAttached); + SubscribeLocalEvent(OnStatusEffectContainerDetached); + SubscribeLocalEvent(OnGetState); + + _containerQuery = GetEntityQuery(); + _effectQuery = GetEntityQuery(); + } + + private void OnGetState(Entity ent, ref ComponentGetState args) + { + args.State = new StatusEffectContainerComponentState(GetNetEntitySet(ent.Comp.ActiveStatusEffects)); + } + + private void OnStatusEffectContainerAttached(Entity ent, ref LocalPlayerAttachedEvent args) + { + foreach (var effect in ent.Comp.ActiveStatusEffects) + { + var ev = new StatusEffectPlayerAttachedEvent(ent); + RaiseLocalEvent(effect, ref ev); + } + } + + private void OnStatusEffectContainerDetached(Entity ent, ref LocalPlayerDetachedEvent args) + { + foreach (var effect in ent.Comp.ActiveStatusEffects) + { + var ev = new StatusEffectPlayerDetachedEvent(ent); + RaiseLocalEvent(effect, ref ev); + } + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var ent, out var effect)) + { + if (effect.EndEffectTime is null) + continue; + + if (!(_timing.CurTime >= effect.EndEffectTime)) + continue; + + if (effect.AppliedTo is null) + continue; + + var meta = MetaData(ent); + if (meta.EntityPrototype is null) + continue; + + TryRemoveStatusEffect(effect.AppliedTo.Value, meta.EntityPrototype); + } + } + + private void AddStatusEffectTime(EntityUid effect, TimeSpan delta) + { + if (!_effectQuery.TryComp(effect, out var effectComp)) + return; + + effectComp.EndEffectTime += delta; + Dirty(effect, effectComp); + + if (effectComp is { AppliedTo: not null, Alert: not null }) + { + (TimeSpan Start, TimeSpan End)? cooldown = effectComp.EndEffectTime is null + ? null + : (_timing.CurTime, effectComp.EndEffectTime.Value); + _alerts.ShowAlert( + effectComp.AppliedTo.Value, + effectComp.Alert.Value, + cooldown: cooldown + ); + } + } + + private void SetStatusEffectTime(EntityUid effect, TimeSpan duration) + { + if (!_effectQuery.TryComp(effect, out var effectComp)) + return; + + effectComp.EndEffectTime = _timing.CurTime + duration; + Dirty(effect, effectComp); + + if (effectComp is { AppliedTo: not null, Alert: not null }) + { + (TimeSpan, TimeSpan)? cooldown = effectComp.EndEffectTime is null + ? null + : (_timing.CurTime, effectComp.EndEffectTime.Value); + _alerts.ShowAlert( + effectComp.AppliedTo.Value, + effectComp.Alert.Value, + cooldown: cooldown + ); + } + } + + private void OnStatusEffectApplied(Entity ent, ref StatusEffectAppliedEvent args) + { + if (ent.Comp is { AppliedTo: not null, Alert: not null }) + { + (TimeSpan, TimeSpan)? cooldown = ent.Comp.EndEffectTime is null + ? null + : (_timing.CurTime, ent.Comp.EndEffectTime.Value); + _alerts.ShowAlert( + ent.Comp.AppliedTo.Value, + ent.Comp.Alert.Value, + cooldown: cooldown + ); + } + } + + private void OnStatusEffectRemoved(Entity ent, ref StatusEffectRemovedEvent args) + { + if (ent.Comp.AppliedTo is null) + return; + + if (ent.Comp is { AppliedTo: not null, Alert: not null }) + _alerts.ClearAlert(ent.Comp.AppliedTo.Value, ent.Comp.Alert.Value); + } + + private bool CanAddStatusEffect(EntityUid uid, EntProtoId effectProto) + { + if (!_proto.TryIndex(effectProto, out var effectProtoData)) + return false; + + if (!effectProtoData.TryGetComponent(out var effectProtoComp, _compFactory)) + return false; + + if (!_whitelist.CheckBoth(uid, effectProtoComp.Blacklist, effectProtoComp.Whitelist)) + return false; + + var ev = new BeforeStatusEffectAddedEvent(effectProto); + RaiseLocalEvent(uid, ref ev); + + if (ev.Cancelled) + return false; + + return true; + } +} + +/// +/// Calls on effect entity, when a status effect is applied. +/// +[ByRefEvent] +public readonly record struct StatusEffectAppliedEvent(EntityUid Target); + +/// +/// Calls on effect entity, when a status effect is removed. +/// +[ByRefEvent] +public readonly record struct StatusEffectRemovedEvent(EntityUid Target); + +/// +/// Called on a status effect entity inside +/// after a player has been to this container entity. +/// +[ByRefEvent] +public readonly record struct StatusEffectPlayerAttachedEvent(EntityUid Target); + +/// +/// Called on a status effect entity inside +/// after a player has been to this container entity. +/// +[ByRefEvent] +public readonly record struct StatusEffectPlayerDetachedEvent(EntityUid Target); + +/// +/// Raised on an entity before a status effect is added to determine if adding it should be cancelled. +/// +[ByRefEvent] +public record struct BeforeStatusEffectAddedEvent(EntProtoId Effect, bool Cancelled = false); diff --git a/Content.Shared/StatusEffectNew/StatusEffectNewSystem.API.cs b/Content.Shared/StatusEffectNew/StatusEffectNewSystem.API.cs new file mode 100644 index 0000000000..8f34f97930 --- /dev/null +++ b/Content.Shared/StatusEffectNew/StatusEffectNewSystem.API.cs @@ -0,0 +1,265 @@ +using System.Diagnostics.CodeAnalysis; +using Content.Shared.StatusEffectNew.Components; +using Robust.Shared.Prototypes; + +namespace Content.Shared.StatusEffectNew; + +public abstract partial class SharedStatusEffectsSystem +{ + /// + /// Attempts to add a status effect to the specified entity. Returns True if the effect is added or it already exists + /// and has been successfully extended in time, returns False if the status effect cannot be applied to this entity, + /// or for any other reason. + /// + /// The target entity to which the effect should be added. + /// ProtoId of the status effect entity. Make sure it has StatusEffectComponent on it. + /// Duration of status effect. Leave null and the effect will be permanent until it is removed using TryRemoveStatusEffect. + /// + /// If True, the effect duration time will be reset and reapplied. If False, the effect duration time will be overlaid with the existing one. + /// In the other case, the effect will either be added for the specified time or its time will be extended for the specified time. + /// + public bool TryAddStatusEffect( + EntityUid target, + EntProtoId effectProto, + TimeSpan? duration = null, + bool resetCooldown = false + ) + { + if (TryGetStatusEffect(target, effectProto, out var existedEffect)) + { + //We don't need to add the effect if it already exists + if (duration is null) + return true; + + if (resetCooldown) + SetStatusEffectTime(existedEffect.Value, duration.Value); + else + AddStatusEffectTime(existedEffect.Value, duration.Value); + + return true; + } + + if (!CanAddStatusEffect(target, effectProto)) + return false; + + var container = EnsureComp(target); + + //And only if all checks passed we spawn the effect + var effect = PredictedSpawnAttachedTo(effectProto, Transform(target).Coordinates); + _transform.SetParent(effect, target); + if (!_effectQuery.TryComp(effect, out var effectComp)) + return false; + + if (duration != null) + effectComp.EndEffectTime = _timing.CurTime + duration; + + container.ActiveStatusEffects.Add(effect); + effectComp.AppliedTo = target; + Dirty(target, container); + Dirty(effect, effectComp); + + var ev = new StatusEffectAppliedEvent(target); + RaiseLocalEvent(effect, ref ev); + + return true; + } + + /// + /// Attempting to remove a status effect from an entity. + /// Returns True if the status effect existed on the entity and was successfully removed, and False in otherwise. + /// + public bool TryRemoveStatusEffect(EntityUid target, EntProtoId effectProto) + { + if (_net.IsClient) //We cant remove the effect on the client (we need someone more robust at networking than me) + return false; + + if (!_containerQuery.TryComp(target, out var container)) + return false; + + foreach (var effect in container.ActiveStatusEffects) + { + var meta = MetaData(effect); + if (meta.EntityPrototype is not null && meta.EntityPrototype == effectProto) + { + if (!_effectQuery.TryComp(effect, out var effectComp)) + return false; + + var ev = new StatusEffectRemovedEvent(target); + RaiseLocalEvent(effect, ref ev); + + QueueDel(effect); + container.ActiveStatusEffects.Remove(effect); + Dirty(target, container); + return true; + } + } + + return false; + } + + /// + /// Checks whether the specified entity is under a specific status effect. + /// + public bool HasStatusEffect(EntityUid target, EntProtoId effectProto) + { + if (!_containerQuery.TryComp(target, out var container)) + return false; + + foreach (var effect in container.ActiveStatusEffects) + { + var meta = MetaData(effect); + if (meta.EntityPrototype is not null && meta.EntityPrototype == effectProto) + return true; + } + + return false; + } + + /// + /// Attempting to retrieve the EntityUid of a status effect from an entity. + /// + public bool TryGetStatusEffect(EntityUid target, EntProtoId effectProto, [NotNullWhen(true)] out EntityUid? effect) + { + effect = null; + if (!_containerQuery.TryComp(target, out var container)) + return false; + + foreach (var e in container.ActiveStatusEffects) + { + var meta = MetaData(e); + if (meta.EntityPrototype is not null && meta.EntityPrototype == effectProto) + { + effect = e; + return true; + } + } + + return false; + } + + /// + /// Attempting to retrieve the time of a status effect from an entity. + /// + /// The target entity on which the effect is applied. + /// The prototype ID of the status effect to retrieve. + /// The output tuple containing the effect entity and its remaining time. + /// Optional. The status effect container component of the entity. + public bool TryGetTime( + EntityUid uid, + EntProtoId effectProto, + out (EntityUid EffectEnt, TimeSpan? EndEffectTime) time, + StatusEffectContainerComponent? container = null + ) + { + time = default; + if (!Resolve(uid, ref container)) + return false; + + foreach (var effect in container.ActiveStatusEffects) + { + var meta = MetaData(effect); + if (meta.EntityPrototype is not null && meta.EntityPrototype == effectProto) + { + if (!_effectQuery.TryComp(effect, out var effectComp)) + return false; + + time = (effect, effectComp.EndEffectTime); + return true; + } + } + + return false; + } + + /// + /// Attempts to edit the remaining time for a status effect on an entity. + /// + /// The target entity on which the effect is applied. + /// The prototype ID of the status effect to modify. + /// + /// The time adjustment to apply to the status effect. Positive values extend the duration, + /// while negative values reduce it. + /// + /// True if duration was edited successfully, false otherwise. + public bool TryAddTime(EntityUid uid, EntProtoId effectProto, TimeSpan time) + { + if (!_containerQuery.TryComp(uid, out var container)) + return false; + + foreach (var effect in container.ActiveStatusEffects) + { + var meta = MetaData(effect); + if (meta.EntityPrototype is not null && meta.EntityPrototype == effectProto) + { + AddStatusEffectTime(effect, time); + return true; + } + } + return false; + } + + /// + /// Attempts to set the remaining time for a status effect on an entity. + /// + /// The target entity on which the effect is applied. + /// The prototype ID of the status effect to modify. + /// The new duration for the status effect. + /// True if duration was set successfully, false otherwise. + public bool TrySetTime(EntityUid uid, EntProtoId effectProto, TimeSpan time) + { + if (!_containerQuery.TryComp(uid, out var container)) + return false; + + foreach (var effect in container.ActiveStatusEffects) + { + var meta = MetaData(effect); + if (meta.EntityPrototype is not null && meta.EntityPrototype == effectProto) + { + SetStatusEffectTime(effect, time); + return true; + } + } + return false; + } + + /// + /// Checks if the specified component is present on any of the entity's status effects. + /// + public bool HasEffectComp(EntityUid? target) where T : IComponent + { + if (!_containerQuery.TryComp(target, out var container)) + return false; + + foreach (var effect in container.ActiveStatusEffects) + { + if (HasComp(effect)) + return true; + } + + return false; + } + + /// + /// Returns all status effects that have the specified component. + /// + public bool TryEffectsWithComp(EntityUid? target, [NotNullWhen(true)] out HashSet>? effects) where T : IComponent + { + effects = null; + if (!_containerQuery.TryComp(target, out var container)) + return false; + + foreach (var effect in container.ActiveStatusEffects) + { + if (!TryComp(effect, out var statusComp)) + continue; + + if (TryComp(effect, out var comp)) + { + effects ??= []; + effects.Add((effect, comp, statusComp)); + } + } + + return effects != null; + } +} diff --git a/Resources/Locale/en-US/entity-categories.ftl b/Resources/Locale/en-US/entity-categories.ftl index a5ed66dd01..8a4ea73d92 100644 --- a/Resources/Locale/en-US/entity-categories.ftl +++ b/Resources/Locale/en-US/entity-categories.ftl @@ -4,5 +4,6 @@ entity-category-name-objectives = Objectives entity-category-name-roles = Mind Roles entity-category-name-mapping = Mapping entity-category-name-donotmap = Do not map +entity-category-name-status-effects = Status Effects entity-category-suffix-donotmap = DO NOT MAP diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/asteroid.yml b/Resources/Prototypes/Entities/Mobs/NPCs/asteroid.yml index 3ba17c3319..de8f15f4c0 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/asteroid.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/asteroid.yml @@ -25,7 +25,6 @@ - Electrocution - TemporaryBlindness - RadiationProtection - - Drowsiness - Adrenaline - type: StandingState - type: Tag diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml b/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml index 1143f8be6c..bb095bfffd 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml @@ -22,12 +22,10 @@ - SlowedDown - Stutter - Electrocution - - ForcedSleep - TemporaryBlindness - Pacified - Flashed - RadiationProtection - - Drowsiness - Adrenaline - type: Buckle - type: StandingState @@ -100,13 +98,11 @@ - SlowedDown - Stutter - Electrocution - - ForcedSleep - TemporaryBlindness - Pacified - StaminaModifier - Flashed - RadiationProtection - - Drowsiness - Adrenaline - type: Bloodstream bloodMaxVolume: 150 diff --git a/Resources/Prototypes/Entities/Mobs/Player/dragon.yml b/Resources/Prototypes/Entities/Mobs/Player/dragon.yml index 7ba6acf50d..fc2dddb23d 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/dragon.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/dragon.yml @@ -103,11 +103,9 @@ - SlowedDown - Stutter - Electrocution - - ForcedSleep - TemporaryBlindness - Pacified - RadiationProtection - - Drowsiness - Adrenaline - type: Temperature heatDamageThreshold: 800 diff --git a/Resources/Prototypes/Entities/Mobs/Species/base.yml b/Resources/Prototypes/Entities/Mobs/Species/base.yml index ed5d53aa97..b93ed915e2 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/base.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/base.yml @@ -130,13 +130,11 @@ - RatvarianLanguage - PressureImmunity - Muted - - ForcedSleep - TemporaryBlindness - Pacified - StaminaModifier - Flashed - RadiationProtection - - Drowsiness - Adrenaline - type: Body prototype: Human diff --git a/Resources/Prototypes/Entities/StatusEffects/misc.yml b/Resources/Prototypes/Entities/StatusEffects/misc.yml new file mode 100644 index 0000000000..254d1608bd --- /dev/null +++ b/Resources/Prototypes/Entities/StatusEffects/misc.yml @@ -0,0 +1,36 @@ +- type: entity + id: StatusEffectBase + abstract: true + components: + - type: StatusEffect + - type: Sprite + drawdepth: Effects + - type: Tag + tags: + - HideContextMenu + +- type: entity + parent: StatusEffectBase + id: MobStatusEffectBase + abstract: true + components: + - type: StatusEffect + whitelist: + components: + - MobState + +# The creature sleeps so heavily that nothing can wake him up. Not even its own death. +- type: entity + parent: MobStatusEffectBase + id: StatusEffectForcedSleeping + name: forced sleep + components: + - type: ForcedSleepingStatusEffect + +# Blurs your vision and makes you randomly fall asleep +- type: entity + parent: MobStatusEffectBase + id: StatusEffectDrowsiness + name: drowsiness + components: + - type: DrowsinessStatusEffect diff --git a/Resources/Prototypes/Entities/categories.yml b/Resources/Prototypes/Entities/categories.yml index 5b8e794309..aaf77989cd 100644 --- a/Resources/Prototypes/Entities/categories.yml +++ b/Resources/Prototypes/Entities/categories.yml @@ -27,3 +27,8 @@ id: DoNotMap name: entity-category-name-donotmap suffix: entity-category-suffix-donotmap + +- type: entityCategory + id: StatusEffects + name: entity-category-name-status-effects + hideSpawnMenu: true \ No newline at end of file diff --git a/Resources/Prototypes/Reagents/Consumable/Drink/alcohol.yml b/Resources/Prototypes/Reagents/Consumable/Drink/alcohol.yml index 7294972e7e..adb9aa28e4 100644 --- a/Resources/Prototypes/Reagents/Consumable/Drink/alcohol.yml +++ b/Resources/Prototypes/Reagents/Consumable/Drink/alcohol.yml @@ -2018,9 +2018,9 @@ - !type:AdjustReagent reagent: Theobromine amount: 0.05 - - !type:GenericStatusEffect - key: Drowsiness - time: 1.0 + - !type:ModifyStatusEffect + effectProto: StatusEffectDrowsiness + time: 1 type: Remove fizziness: 0.25 @@ -2049,9 +2049,9 @@ - !type:AdjustReagent reagent: Theobromine amount: 0.05 - - !type:GenericStatusEffect - key: Drowsiness - time: 1.0 + - !type:ModifyStatusEffect + effectProto: StatusEffectDrowsiness + time: 1 type: Remove fizziness: 0.15 @@ -2080,9 +2080,9 @@ - !type:AdjustReagent reagent: Theobromine amount: 0.05 - - !type:GenericStatusEffect - key: Drowsiness - time: 1.0 + - !type:ModifyStatusEffect + effectProto: StatusEffectDrowsiness + time: 1 type: Remove fizziness: 0.15 @@ -2111,9 +2111,9 @@ - !type:AdjustReagent reagent: Theobromine amount: 0.05 - - !type:GenericStatusEffect - key: Drowsiness - time: 1.0 + - !type:ModifyStatusEffect + effectProto: StatusEffectDrowsiness + time: 1 type: Remove fizziness: 0.25 @@ -2142,9 +2142,9 @@ - !type:AdjustReagent reagent: Theobromine amount: 0.05 - - !type:GenericStatusEffect - key: Drowsiness - time: 1.0 + - !type:ModifyStatusEffect + effectProto: StatusEffectDrowsiness + time: 1 type: Remove fizziness: 0.15 @@ -2173,9 +2173,9 @@ - !type:AdjustReagent reagent: Theobromine amount: 0.05 - - !type:GenericStatusEffect - key: Drowsiness - time: 1.0 + - !type:ModifyStatusEffect + effectProto: StatusEffectDrowsiness + time: 1 type: Remove fizziness: 0.25 diff --git a/Resources/Prototypes/Reagents/Consumable/Drink/drinks.yml b/Resources/Prototypes/Reagents/Consumable/Drink/drinks.yml index 1f092032d0..2d8351c97f 100644 --- a/Resources/Prototypes/Reagents/Consumable/Drink/drinks.yml +++ b/Resources/Prototypes/Reagents/Consumable/Drink/drinks.yml @@ -12,9 +12,9 @@ effects: - !type:SatiateThirst factor: 2 - - !type:GenericStatusEffect - key: Drowsiness - time: 2.0 + - !type:ModifyStatusEffect + effectProto: StatusEffectDrowsiness + time: 2 type: Remove - !type:AdjustReagent reagent: Theobromine @@ -105,9 +105,9 @@ effects: - !type:SatiateThirst factor: 2 - - !type:GenericStatusEffect - key: Drowsiness - time: 2.0 + - !type:ModifyStatusEffect + effectProto: StatusEffectDrowsiness + time: 2 type: Remove - type: reagent @@ -161,9 +161,9 @@ effects: - !type:SatiateThirst factor: 2 - - !type:GenericStatusEffect - key: Drowsiness - time: 2.0 + - !type:ModifyStatusEffect + effectProto: StatusEffectDrowsiness + time: 2 type: Remove - type: reagent @@ -342,9 +342,9 @@ effects: - !type:SatiateThirst factor: 6 - - !type:GenericStatusEffect - key: Drowsiness - time: 3.0 + - !type:ModifyStatusEffect + effectProto: StatusEffectDrowsiness + time: 3 type: Remove Poison: effects: @@ -383,9 +383,9 @@ effects: - !type:SatiateThirst factor: 2 - - !type:GenericStatusEffect - key: Drowsiness - time: 2.0 + - !type:ModifyStatusEffect + effectProto: StatusEffectDrowsiness + time: 2 type: Remove - type: reagent diff --git a/Resources/Prototypes/Reagents/Consumable/Drink/soda.yml b/Resources/Prototypes/Reagents/Consumable/Drink/soda.yml index d224d1573e..5ac667ec18 100644 --- a/Resources/Prototypes/Reagents/Consumable/Drink/soda.yml +++ b/Resources/Prototypes/Reagents/Consumable/Drink/soda.yml @@ -18,9 +18,9 @@ effects: - !type:SatiateThirst factor: 2 - - !type:GenericStatusEffect - key: Drowsiness - time: 1.0 + - !type:ModifyStatusEffect + effectProto: StatusEffectDrowsiness + time: 1 type: Remove - type: reagent @@ -80,9 +80,9 @@ effects: - !type:SatiateThirst factor: 2 - - !type:GenericStatusEffect - key: Drowsiness - time: 2.0 + - !type:ModifyStatusEffect + effectProto: StatusEffectDrowsiness + time: 2 type: Remove - !type:AdjustReagent reagent: Theobromine @@ -261,7 +261,7 @@ state: icon_empty metamorphicMaxFillLevels: 5 metamorphicFillBaseName: fill- - metamorphicChangeColor: false + metamorphicChangeColor: false metabolisms: Drink: effects: diff --git a/Resources/Prototypes/Reagents/gases.yml b/Resources/Prototypes/Reagents/gases.yml index 2677fc0ed0..1087d7bad4 100644 --- a/Resources/Prototypes/Reagents/gases.yml +++ b/Resources/Prototypes/Reagents/gases.yml @@ -360,7 +360,7 @@ shouldHave: false walkSpeedModifier: 0.65 sprintSpeedModifier: 0.65 - - !type:GenericStatusEffect + - !type:ModifyStatusEffect conditions: - !type:ReagentThreshold reagent: NitrousOxide @@ -368,8 +368,7 @@ - !type:OrganType type: Slime shouldHave: false - key: ForcedSleep - component: ForcedSleeping + effectProto: StatusEffectForcedSleeping time: 3 type: Add - !type:HealthChange diff --git a/Resources/Prototypes/Reagents/medicine.yml b/Resources/Prototypes/Reagents/medicine.yml index d553b8065d..2873372b52 100644 --- a/Resources/Prototypes/Reagents/medicine.yml +++ b/Resources/Prototypes/Reagents/medicine.yml @@ -83,9 +83,8 @@ key: Jitter time: 3.0 type: Remove - - !type:GenericStatusEffect - key: Drowsiness - component: Drowsiness + - !type:ModifyStatusEffect + effectProto: StatusEffectDrowsiness time: 1.5 type: Add refresh: false @@ -923,8 +922,8 @@ - !type:GenericStatusEffect key: Stutter component: StutteringAccent - - !type:GenericStatusEffect - key: Drowsiness + - !type:ModifyStatusEffect + effectProto: StatusEffectDrowsiness time: 10 type: Remove - !type:ResetNarcolepsy @@ -948,8 +947,8 @@ metabolisms: Medicine: effects: - - !type:GenericStatusEffect - key: Drowsiness + - !type:ModifyStatusEffect + effectProto: StatusEffectDrowsiness time: 10 type: Remove - !type:ResetNarcolepsy @@ -1390,9 +1389,8 @@ emote: Yawn showInChat: true probability: 0.1 - - !type:GenericStatusEffect - key: Drowsiness - component: Drowsiness + - !type:ModifyStatusEffect + effectProto: StatusEffectDrowsiness time: 4 type: Add refresh: false diff --git a/Resources/Prototypes/Reagents/narcotics.yml b/Resources/Prototypes/Reagents/narcotics.yml index 6e159278e9..aff87479b1 100644 --- a/Resources/Prototypes/Reagents/narcotics.yml +++ b/Resources/Prototypes/Reagents/narcotics.yml @@ -40,12 +40,12 @@ key: KnockedDown time: 3 type: Remove - - !type:GenericStatusEffect + - !type:ModifyStatusEffect conditions: - !type:ReagentThreshold reagent: Haloperidol max: 0.01 - key: Drowsiness + effectProto: StatusEffectDrowsiness time: 10 type: Remove Medicine: @@ -88,12 +88,12 @@ key: KnockedDown time: 1 type: Remove - - !type:GenericStatusEffect + - !type:ModifyStatusEffect conditions: - !type:ReagentThreshold reagent: Haloperidol max: 0.01 - key: Drowsiness + effectProto: StatusEffectDrowsiness time: 10 type: Remove - !type:PopupMessage @@ -152,16 +152,16 @@ component: StaminaModifier time: 3 type: Add - - !type:GenericStatusEffect - key: ForcedSleep + - !type:ModifyStatusEffect + effectProto: StatusEffectForcedSleeping time: 3 type: Remove - - !type:GenericStatusEffect + - !type:ModifyStatusEffect conditions: - !type:ReagentThreshold reagent: Haloperidol max: 0.01 - key: Drowsiness + effectProto: StatusEffectDrowsiness time: 10 type: Remove Medicine: @@ -296,14 +296,13 @@ metabolisms: Narcotic: effects: - - !type:GenericStatusEffect + - !type:ModifyStatusEffect conditions: - !type:ReagentThreshold reagent: Nocturine min: 8 - key: ForcedSleep - component: ForcedSleeping - refresh: false + effectProto: StatusEffectForcedSleeping + time: 3 type: Add - type: reagent diff --git a/Resources/Prototypes/Reagents/toxins.yml b/Resources/Prototypes/Reagents/toxins.yml index 72730c7990..6560f05995 100644 --- a/Resources/Prototypes/Reagents/toxins.yml +++ b/Resources/Prototypes/Reagents/toxins.yml @@ -63,9 +63,8 @@ - !type:MovespeedModifier walkSpeedModifier: 0.65 sprintSpeedModifier: 0.65 - - !type:GenericStatusEffect - key: Drowsiness - component: Drowsiness + - !type:ModifyStatusEffect + effectProto: StatusEffectDrowsiness time: 4 type: Add refresh: false diff --git a/Resources/Prototypes/status_effects.yml b/Resources/Prototypes/status_effects.yml index 49e5ccc579..e98dd4df02 100644 --- a/Resources/Prototypes/status_effects.yml +++ b/Resources/Prototypes/status_effects.yml @@ -1,6 +1,9 @@ # Status effect prototypes. # Holds no actual logic, just some basic data about the effect. +# Note: We have a new status effect system that needs all of these status effects to be fully ported to. +# Adding new status effects under the old system is NOT RECOMMENDED. + - type: statusEffect id: Stun alert: Stun @@ -45,9 +48,6 @@ id: Corporeal alert: Corporeal -- type: statusEffect - id: ForcedSleep #I.e., they will not wake on damage or similar - - type: statusEffect id: TemporaryBlindness @@ -66,9 +66,6 @@ - type: statusEffect id: RadiationProtection -- type: statusEffect - id: Drowsiness #blurs your vision and makes you randomly fall asleep - - type: statusEffect id: Adrenaline alert: Adrenaline