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<T, StatusEffectComponent>

---------

Co-authored-by: pa.pecherskij <pa.pecherskij@interfax.ru>
This commit is contained in:
Red
2025-06-25 14:41:35 +03:00
committed by GitHub
parent 8be0b7a614
commit 78a94730be
35 changed files with 913 additions and 170 deletions

View File

@@ -1,5 +1,7 @@
using Content.Shared.Bed.Sleep;
using Content.Shared.Drowsiness; using Content.Shared.Drowsiness;
using Content.Shared.StatusEffect; using Content.Shared.StatusEffectNew;
using Content.Shared.StatusEffectNew.Components;
using Robust.Client.Graphics; using Robust.Client.Graphics;
using Robust.Client.Player; using Robust.Client.Player;
using Robust.Shared.Enums; using Robust.Shared.Enums;
@@ -15,11 +17,14 @@ public sealed class DrowsinessOverlay : Overlay
[Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IEntitySystemManager _sysMan = default!; [Dependency] private readonly IEntitySystemManager _sysMan = default!;
[Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IGameTiming _timing = default!;
private readonly SharedStatusEffectsSystem _statusEffects = default!;
public override OverlaySpace Space => OverlaySpace.WorldSpace; public override OverlaySpace Space => OverlaySpace.WorldSpace;
public override bool RequestScreenTexture => true; public override bool RequestScreenTexture => true;
private readonly ShaderInstance _drowsinessShader; private readonly ShaderInstance _drowsinessShader;
private EntityQuery<StatusEffectComponent> _statusQuery;
public float CurrentPower = 0.0f; public float CurrentPower = 0.0f;
private const float PowerDivisor = 250.0f; private const float PowerDivisor = 250.0f;
@@ -29,6 +34,9 @@ public sealed class DrowsinessOverlay : Overlay
public DrowsinessOverlay() public DrowsinessOverlay()
{ {
IoCManager.InjectDependencies(this); IoCManager.InjectDependencies(this);
_statusEffects = _sysMan.GetEntitySystem<SharedStatusEffectsSystem>();
_statusQuery = _entityManager.GetEntityQuery<StatusEffectComponent>();
_drowsinessShader = _prototypeManager.Index<ShaderPrototype>("Drowsiness").InstanceUnique(); _drowsinessShader = _prototypeManager.Index<ShaderPrototype>("Drowsiness").InstanceUnique();
} }
@@ -39,16 +47,21 @@ public sealed class DrowsinessOverlay : Overlay
if (playerEntity == null) if (playerEntity == null)
return; return;
if (!_entityManager.HasComponent<DrowsinessComponent>(playerEntity) if (!_statusEffects.TryEffectsWithComp<DrowsinessStatusEffectComponent>(playerEntity, out var drowsinessEffects))
|| !_entityManager.TryGetComponent<StatusEffectsComponent>(playerEntity, out var status))
return; return;
var statusSys = _sysMan.GetEntitySystem<StatusEffectsSystem>(); TimeSpan? remainingTime = TimeSpan.Zero;
if (!statusSys.TryGetTime(playerEntity.Value, SharedDrowsinessSystem.DrowsinessKey, out var time, status)) foreach (var (_, _, statusEffectComp) in drowsinessEffects)
{
if (statusEffectComp.EndEffectTime > remainingTime)
remainingTime = statusEffectComp.EndEffectTime;
}
if (remainingTime is null)
return; return;
var curTime = _timing.CurTime; 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); CurrentPower += 8f * (0.5f * timeLeft - CurrentPower) * args.DeltaSeconds / (timeLeft + 1);
} }

View File

@@ -1,7 +1,7 @@
using Content.Shared.Drowsiness; using Content.Shared.Drowsiness;
using Content.Shared.StatusEffectNew;
using Robust.Client.Graphics; using Robust.Client.Graphics;
using Robust.Client.Player; using Robust.Client.Player;
using Robust.Shared.Player;
namespace Content.Client.Drowsiness; namespace Content.Client.Drowsiness;
@@ -9,6 +9,7 @@ public sealed class DrowsinessSystem : SharedDrowsinessSystem
{ {
[Dependency] private readonly IPlayerManager _player = default!; [Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IOverlayManager _overlayMan = default!; [Dependency] private readonly IOverlayManager _overlayMan = default!;
[Dependency] private readonly SharedStatusEffectsSystem _statusEffects = default!;
private DrowsinessOverlay _overlay = default!; private DrowsinessOverlay _overlay = default!;
@@ -16,35 +17,47 @@ public sealed class DrowsinessSystem : SharedDrowsinessSystem
{ {
base.Initialize(); base.Initialize();
SubscribeLocalEvent<DrowsinessComponent, ComponentInit>(OnDrowsinessInit); SubscribeLocalEvent<DrowsinessStatusEffectComponent, StatusEffectAppliedEvent>(OnDrowsinessApply);
SubscribeLocalEvent<DrowsinessComponent, ComponentShutdown>(OnDrowsinessShutdown); SubscribeLocalEvent<DrowsinessStatusEffectComponent, StatusEffectRemovedEvent>(OnDrowsinessShutdown);
SubscribeLocalEvent<DrowsinessComponent, LocalPlayerAttachedEvent>(OnPlayerAttached); SubscribeLocalEvent<DrowsinessStatusEffectComponent, StatusEffectPlayerAttachedEvent>(OnStatusEffectPlayerAttached);
SubscribeLocalEvent<DrowsinessComponent, LocalPlayerDetachedEvent>(OnPlayerDetached); SubscribeLocalEvent<DrowsinessStatusEffectComponent, StatusEffectPlayerDetachedEvent>(OnStatusEffectPlayerDetached);
_overlay = new(); _overlay = new();
} }
private void OnPlayerAttached(EntityUid uid, DrowsinessComponent component, LocalPlayerAttachedEvent args) private void OnDrowsinessApply(Entity<DrowsinessStatusEffectComponent> ent, ref StatusEffectAppliedEvent args)
{ {
if (_player.LocalEntity == args.Target)
_overlayMan.AddOverlay(_overlay); _overlayMan.AddOverlay(_overlay);
} }
private void OnPlayerDetached(EntityUid uid, DrowsinessComponent component, LocalPlayerDetachedEvent args) private void OnDrowsinessShutdown(Entity<DrowsinessStatusEffectComponent> ent, ref StatusEffectRemovedEvent args)
{
if (_player.LocalEntity != args.Target)
return;
if (!_statusEffects.HasEffectComp<DrowsinessStatusEffectComponent>(_player.LocalEntity.Value))
{ {
_overlay.CurrentPower = 0; _overlay.CurrentPower = 0;
_overlayMan.RemoveOverlay(_overlay); _overlayMan.RemoveOverlay(_overlay);
} }
}
private void OnDrowsinessInit(EntityUid uid, DrowsinessComponent component, ComponentInit args) private void OnStatusEffectPlayerAttached(Entity<DrowsinessStatusEffectComponent> ent, ref StatusEffectPlayerAttachedEvent args)
{ {
if (_player.LocalEntity == uid) if (_player.LocalEntity != args.Target)
return;
_overlayMan.AddOverlay(_overlay); _overlayMan.AddOverlay(_overlay);
} }
private void OnDrowsinessShutdown(EntityUid uid, DrowsinessComponent component, ComponentShutdown args) private void OnStatusEffectPlayerDetached(Entity<DrowsinessStatusEffectComponent> ent, ref StatusEffectPlayerDetachedEvent args)
{ {
if (_player.LocalEntity == uid) if (_player.LocalEntity != args.Target)
return;
if (!_statusEffects.HasEffectComp<DrowsinessStatusEffectComponent>(_player.LocalEntity.Value))
{ {
_overlay.CurrentPower = 0; _overlay.CurrentPower = 0;
_overlayMan.RemoveOverlay(_overlay); _overlayMan.RemoveOverlay(_overlay);

View File

@@ -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;
/// <inheritdoc/>
public sealed partial class ClientStatusEffectsSystem : SharedStatusEffectsSystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<StatusEffectContainerComponent, ComponentHandleState>(OnHandleState);
}
private void OnHandleState(Entity<StatusEffectContainerComponent> ent, ref ComponentHandleState args)
{
if (args.Current is not StatusEffectContainerComponentState state)
return;
var toRemove = new ValueList<EntityUid>();
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);
}
}
}

View File

@@ -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.Drowsiness;
using Content.Shared.StatusEffect; using Content.Shared.StatusEffectNew;
using Content.Shared.StatusEffectNew.Components;
using Robust.Shared.Random; using Robust.Shared.Random;
using Robust.Shared.Timing; using Robust.Shared.Timing;
@@ -8,9 +10,6 @@ namespace Content.Server.Drowsiness;
public sealed class DrowsinessSystem : SharedDrowsinessSystem public sealed class DrowsinessSystem : SharedDrowsinessSystem
{ {
[ValidatePrototypeId<StatusEffectPrototype>]
private const string SleepKey = "ForcedSleep"; // Same one used by N2O and other sleep chems.
[Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly StatusEffectsSystem _statusEffects = default!; [Dependency] private readonly StatusEffectsSystem _statusEffects = default!;
@@ -18,33 +17,37 @@ public sealed class DrowsinessSystem : SharedDrowsinessSystem
/// <inheritdoc/> /// <inheritdoc/>
public override void Initialize() public override void Initialize()
{ {
SubscribeLocalEvent<DrowsinessComponent, ComponentStartup>(OnInit); SubscribeLocalEvent<DrowsinessStatusEffectComponent, StatusEffectAppliedEvent>(OnEffectApplied);
} }
private void OnInit(EntityUid uid, DrowsinessComponent component, ComponentStartup args) private void OnEffectApplied(Entity<DrowsinessStatusEffectComponent> 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) public override void Update(float frameTime)
{ {
base.Update(frameTime); base.Update(frameTime);
var query = EntityQueryEnumerator<DrowsinessComponent>(); var query = EntityQueryEnumerator<DrowsinessStatusEffectComponent, StatusEffectComponent>();
while (query.MoveNext(out var uid, out var component)) 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; continue;
// Set the new time. // 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 // 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. // Make sure the sleep time doesn't cut into the time to next incident.
component.NextIncidentTime += duration; drowsiness.NextIncidentTime += duration;
_statusEffects.TryAddStatusEffect<ForcedSleepingComponent>(uid, SleepKey, duration, false); _statusEffects.TryAddStatusEffect(statusEffect.AppliedTo.Value, SleepingSystem.StatusEffectForcedSleeping, duration);
} }
} }
} }

View File

@@ -0,0 +1,23 @@
using Content.Shared.StatusEffectNew;
using Content.Shared.StatusEffectNew.Components;
namespace Content.Server.StatusEffectNew;
/// <inheritdoc/>
public sealed partial class StatusEffectsSystem : SharedStatusEffectsSystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<StatusEffectContainerComponent, ComponentShutdown>(OnContainerShutdown);
}
private void OnContainerShutdown(Entity<StatusEffectContainerComponent> ent, ref ComponentShutdown args)
{
foreach (var effect in ent.Comp.ActiveStatusEffects)
{
QueueDel(effect);
}
}
}

View File

@@ -1,5 +1,5 @@
using Content.Server.StatusEffectNew;
using Content.Shared.Bed.Sleep; using Content.Shared.Bed.Sleep;
using Content.Shared.StatusEffect;
using Robust.Shared.Random; using Robust.Shared.Random;
namespace Content.Server.Traits.Assorted; namespace Content.Server.Traits.Assorted;
@@ -9,9 +9,6 @@ namespace Content.Server.Traits.Assorted;
/// </summary> /// </summary>
public sealed class NarcolepsySystem : EntitySystem public sealed class NarcolepsySystem : EntitySystem
{ {
[ValidatePrototypeId<StatusEffectPrototype>]
private const string StatusEffectKey = "ForcedSleep"; // Same one used by N2O and other sleep chems.
[Dependency] private readonly StatusEffectsSystem _statusEffects = default!; [Dependency] private readonly StatusEffectsSystem _statusEffects = default!;
[Dependency] private readonly IRobustRandom _random = 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. // Make sure the sleep time doesn't cut into the time to next incident.
narcolepsy.NextIncidentTime += duration; narcolepsy.NextIncidentTime += duration;
_statusEffects.TryAddStatusEffect<ForcedSleepingComponent>(uid, StatusEffectKey, _statusEffects.TryAddStatusEffect(uid, SleepingSystem.StatusEffectForcedSleeping, TimeSpan.FromSeconds(duration));
TimeSpan.FromSeconds(duration), false);
} }
} }
} }

View File

@@ -1,11 +0,0 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Bed.Sleep
{
/// <summary>
/// Prevents waking up. Use as a status effect.
/// </summary>
[NetworkedComponent, RegisterComponent]
public sealed partial class ForcedSleepingComponent : Component
{}
}

View File

@@ -0,0 +1,10 @@
using Content.Shared.StatusEffectNew.Components;
using Robust.Shared.GameStates;
namespace Content.Shared.Bed.Sleep;
/// <summary>
/// Prevents waking up. Use only in conjunction with <see cref="StatusEffectComponent"/>, on the status effect entity.
/// </summary>
[NetworkedComponent, RegisterComponent]
public sealed partial class ForcedSleepingStatusEffectComponent : Component;

View File

@@ -19,6 +19,7 @@ using Content.Shared.Sound;
using Content.Shared.Sound.Components; using Content.Shared.Sound.Components;
using Content.Shared.Speech; using Content.Shared.Speech;
using Content.Shared.StatusEffect; using Content.Shared.StatusEffect;
using Content.Shared.StatusEffectNew;
using Content.Shared.Stunnable; using Content.Shared.Stunnable;
using Content.Shared.Traits.Assorted; using Content.Shared.Traits.Assorted;
using Content.Shared.Verbs; using Content.Shared.Verbs;
@@ -37,10 +38,12 @@ public sealed partial class SleepingSystem : EntitySystem
[Dependency] private readonly SharedPopupSystem _popupSystem = default!; [Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedEmitSoundSystem _emitSound = 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 SleepActionId = "ActionSleep";
public static readonly EntProtoId WakeActionId = "ActionWake"; public static readonly EntProtoId WakeActionId = "ActionWake";
public static readonly EntProtoId StatusEffectForcedSleeping = "StatusEffectForcedSleeping";
public override void Initialize() public override void Initialize()
{ {
@@ -65,7 +68,7 @@ public sealed partial class SleepingSystem : EntitySystem
SubscribeLocalEvent<SleepingComponent, GetVerbsEvent<AlternativeVerb>>(AddWakeVerb); SubscribeLocalEvent<SleepingComponent, GetVerbsEvent<AlternativeVerb>>(AddWakeVerb);
SubscribeLocalEvent<SleepingComponent, InteractHandEvent>(OnInteractHand); SubscribeLocalEvent<SleepingComponent, InteractHandEvent>(OnInteractHand);
SubscribeLocalEvent<ForcedSleepingComponent, ComponentInit>(OnInit); SubscribeLocalEvent<ForcedSleepingStatusEffectComponent, StatusEffectAppliedEvent>(OnStatusEffectApplied);
SubscribeLocalEvent<SleepingComponent, UnbuckleAttemptEvent>(OnUnbuckleAttempt); SubscribeLocalEvent<SleepingComponent, UnbuckleAttemptEvent>(OnUnbuckleAttempt);
SubscribeLocalEvent<SleepingComponent, EmoteAttemptEvent>(OnEmoteAttempt); SubscribeLocalEvent<SleepingComponent, EmoteAttemptEvent>(OnEmoteAttempt);
@@ -104,8 +107,8 @@ public sealed partial class SleepingSystem : EntitySystem
if (args.FellAsleep) if (args.FellAsleep)
{ {
// Expiring status effects would remove the components needed for sleeping // Expiring status effects would remove the components needed for sleeping
_statusEffectsSystem.TryRemoveStatusEffect(ent.Owner, "Stun"); _statusEffectOld.TryRemoveStatusEffect(ent.Owner, "Stun");
_statusEffectsSystem.TryRemoveStatusEffect(ent.Owner, "KnockedDown"); _statusEffectOld.TryRemoveStatusEffect(ent.Owner, "KnockedDown");
EnsureComp<StunnedComponent>(ent); EnsureComp<StunnedComponent>(ent);
EnsureComp<KnockedDownComponent>(ent); EnsureComp<KnockedDownComponent>(ent);
@@ -248,9 +251,9 @@ public sealed partial class SleepingSystem : EntitySystem
_emitSound.SetEnabled((ent, spam), args.NewMobState == MobState.Alive); _emitSound.SetEnabled((ent, spam), args.NewMobState == MobState.Alive);
} }
private void OnInit(Entity<ForcedSleepingComponent> ent, ref ComponentInit args) private void OnStatusEffectApplied(Entity<ForcedSleepingStatusEffectComponent> ent, ref StatusEffectAppliedEvent args)
{ {
TrySleeping(ent.Owner); TrySleeping(args.Target);
} }
private void Wake(Entity<SleepingComponent> ent) private void Wake(Entity<SleepingComponent> ent)
@@ -307,7 +310,7 @@ public sealed partial class SleepingSystem : EntitySystem
if (!Resolve(ent, ref ent.Comp, false)) if (!Resolve(ent, ref ent.Comp, false))
return false; return false;
if (!force && HasComp<ForcedSleepingComponent>(ent)) if (!force && _statusEffectNew.HasEffectComp<ForcedSleepingStatusEffectComponent>(ent))
{ {
if (user != null) if (user != null)
{ {

View File

@@ -3,7 +3,7 @@ using Content.Shared.Damage.Events;
using Content.Shared.Destructible; using Content.Shared.Destructible;
using Content.Shared.Rejuvenate; using Content.Shared.Rejuvenate;
using Content.Shared.Slippery; using Content.Shared.Slippery;
using Content.Shared.StatusEffect; using Content.Shared.StatusEffectNew;
namespace Content.Shared.Damage.Systems; namespace Content.Shared.Damage.Systems;

View File

@@ -1,4 +1,5 @@
using System.Numerics; using System.Numerics;
using Content.Shared.StatusEffectNew.Components;
using Robust.Shared.GameStates; using Robust.Shared.GameStates;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
@@ -6,21 +7,22 @@ namespace Content.Shared.Drowsiness;
/// <summary> /// <summary>
/// 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 <see cref="StatusEffectComponent"/>, on the status effect entity.
/// </summary> /// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentPause] [RegisterComponent, NetworkedComponent, AutoGenerateComponentPause]
public sealed partial class DrowsinessComponent : Component public sealed partial class DrowsinessStatusEffectComponent : Component
{ {
/// <summary> /// <summary>
/// The random time between sleeping incidents, (min, max). /// The random time between sleeping incidents, (min, max).
/// </summary> /// </summary>
[DataField(required: true)] [DataField]
public Vector2 TimeBetweenIncidents = new Vector2(5f, 60f); public Vector2 TimeBetweenIncidents = new(5f, 60f);
/// <summary> /// <summary>
/// The duration of sleeping incidents, (min, max). /// The duration of sleeping incidents, (min, max).
/// </summary> /// </summary>
[DataField(required: true)] [DataField]
public Vector2 DurationOfIncident = new Vector2(2, 5); public Vector2 DurationOfIncident = new(2, 5);
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
[AutoPausedField] [AutoPausedField]

View File

@@ -1,9 +1,5 @@
using Content.Shared.StatusEffect;
namespace Content.Shared.Drowsiness; namespace Content.Shared.Drowsiness;
public abstract class SharedDrowsinessSystem : EntitySystem public abstract class SharedDrowsinessSystem : EntitySystem
{ {
[ValidatePrototypeId<StatusEffectPrototype>]
public const string DrowsinessKey = "Drowsiness";
} }

View File

@@ -13,6 +13,7 @@ namespace Content.Shared.EntityEffects.Effects.StatusEffects;
/// <remarks> /// <remarks>
/// Can be used for things like adding accents or something. I don't know. Go wild. /// Can be used for things like adding accents or something. I don't know. Go wild.
/// </remarks> /// </remarks>
[Obsolete("Use ModifyStatusEffect with StatusEffectNewSystem instead")]
public sealed partial class GenericStatusEffect : EntityEffect public sealed partial class GenericStatusEffect : EntityEffect
{ {
[DataField(required: true)] [DataField(required: true)]

View File

@@ -0,0 +1,66 @@
using Content.Shared.StatusEffectNew;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
namespace Content.Shared.EntityEffects.Effects.StatusEffects;
/// <summary>
/// Changes status effects on entities: Adds, removes or sets time.
/// </summary>
[UsedImplicitly]
public sealed partial class ModifyStatusEffect : EntityEffect
{
[DataField(required: true)]
public EntProtoId EffectProto;
/// <summary>
/// Time for which status effect should be applied. Behaviour changes according to <see cref="Refresh" />.
/// </summary>
[DataField]
public float Time = 2.0f;
/// <remarks>
/// true - refresh status effect time, false - accumulate status effect time.
/// </remarks>
[DataField]
public bool Refresh = true;
/// <summary>
/// Should this effect add the status effect, remove time from it, or set its cooldown?
/// </summary>
[DataField]
public StatusEffectMetabolismType Type = StatusEffectMetabolismType.Add;
/// <inheritdoc />
public override void Effect(EntityEffectBaseArgs args)
{
var statusSys = args.EntityManager.EntitySysManager.GetEntitySystem<SharedStatusEffectsSystem>();
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;
}
}
/// <inheritdoc />
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)
);
}

View File

@@ -1,5 +1,6 @@
using Content.Shared.Bed.Sleep; using Content.Shared.Bed.Sleep;
using Content.Shared.CCVar; using Content.Shared.CCVar;
using Content.Shared.StatusEffectNew;
using Robust.Shared.Configuration; using Robust.Shared.Configuration;
using Robust.Shared.Player; using Robust.Shared.Player;
using Robust.Shared.Timing; using Robust.Shared.Timing;
@@ -13,6 +14,7 @@ public sealed class SSDIndicatorSystem : EntitySystem
{ {
[Dependency] private readonly IConfigurationManager _cfg = default!; [Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly SharedStatusEffectsSystem _statusEffects = default!;
private bool _icSsdSleep; private bool _icSsdSleep;
private float _icSsdSleepTime; private float _icSsdSleepTime;
@@ -37,10 +39,11 @@ public sealed class SSDIndicatorSystem : EntitySystem
component.FallAsleepTime = TimeSpan.Zero; component.FallAsleepTime = TimeSpan.Zero;
if (component.ForcedSleepAdded) // Remove component only if it has been added by this system if (component.ForcedSleepAdded) // Remove component only if it has been added by this system
{ {
EntityManager.RemoveComponent<ForcedSleepingComponent>(uid); _statusEffects.TryRemoveStatusEffect(uid, SleepingSystem.StatusEffectForcedSleeping);
component.ForcedSleepAdded = false; component.ForcedSleepAdded = false;
} }
} }
Dirty(uid, component); Dirty(uid, component);
} }
@@ -53,6 +56,7 @@ public sealed class SSDIndicatorSystem : EntitySystem
{ {
component.FallAsleepTime = _timing.CurTime + TimeSpan.FromSeconds(_icSsdSleepTime); component.FallAsleepTime = _timing.CurTime + TimeSpan.FromSeconds(_icSsdSleepTime);
} }
Dirty(uid, component); Dirty(uid, component);
} }
@@ -79,12 +83,11 @@ public sealed class SSDIndicatorSystem : EntitySystem
while (query.MoveNext(out var uid, out var ssd)) while (query.MoveNext(out var uid, out var ssd))
{ {
// Forces the entity to sleep when the time has come // Forces the entity to sleep when the time has come
if(ssd.IsSSD && if (ssd.IsSSD &&
ssd.FallAsleepTime <= _timing.CurTime && ssd.FallAsleepTime <= _timing.CurTime &&
!TerminatingOrDeleted(uid) && !TerminatingOrDeleted(uid))
!HasComp<ForcedSleepingComponent>(uid)) // Don't add the component if the entity has it from another sources
{ {
EnsureComp<ForcedSleepingComponent>(uid); _statusEffects.TryAddStatusEffect(uid, SleepingSystem.StatusEffectForcedSleeping);
ssd.ForcedSleepAdded = true; ssd.ForcedSleepAdded = true;
} }
} }

View File

@@ -1,6 +1,7 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Content.Shared.Alert; using Content.Shared.Alert;
using Content.Shared.Rejuvenate; using Content.Shared.Rejuvenate;
using Content.Shared.StatusEffectNew;
using Robust.Shared.GameStates; using Robust.Shared.GameStates;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Timing; using Robust.Shared.Timing;
@@ -8,6 +9,7 @@ using Robust.Shared.Utility;
namespace Content.Shared.StatusEffect namespace Content.Shared.StatusEffect
{ {
[Obsolete("Migration to Content.Shared.StatusEffectNew.SharedStatusEffectsSystem is required")]
public sealed class StatusEffectsSystem : EntitySystem public sealed class StatusEffectsSystem : EntitySystem
{ {
[Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
@@ -104,6 +106,7 @@ namespace Content.Shared.StatusEffect
/// <param name="status">The status effects component to change, if you already have it.</param> /// <param name="status">The status effects component to change, if you already have it.</param>
/// <returns>False if the effect could not be added or the component already exists, true otherwise.</returns> /// <returns>False if the effect could not be added or the component already exists, true otherwise.</returns>
/// <typeparam name="T">The component type to add and remove from the entity.</typeparam> /// <typeparam name="T">The component type to add and remove from the entity.</typeparam>
[Obsolete("Migration to Content.Shared.StatusEffectNew.SharedStatusEffectsSystem is required")]
public bool TryAddStatusEffect<T>(EntityUid uid, string key, TimeSpan time, bool refresh, public bool TryAddStatusEffect<T>(EntityUid uid, string key, TimeSpan time, bool refresh,
StatusEffectsComponent? status = null) StatusEffectsComponent? status = null)
where T : IComponent, new() 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, public bool TryAddStatusEffect(EntityUid uid, string key, TimeSpan time, bool refresh, string component,
StatusEffectsComponent? status = null) 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 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! /// If you want special 'effect merging' behavior, do it your own damn self!
/// </remarks> /// </remarks>
[Obsolete("Migration to Content.Shared.StatusEffectNew.SharedStatusEffectsSystem is required")]
public bool TryAddStatusEffect(EntityUid uid, public bool TryAddStatusEffect(EntityUid uid,
string key, string key,
TimeSpan time, TimeSpan time,
@@ -255,6 +260,7 @@ namespace Content.Shared.StatusEffect
/// Obviously this doesn't automatically clear any effects a status effect might have. /// 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. /// That's up to the removed component to handle itself when it's removed.
/// </remarks> /// </remarks>
[Obsolete("Migration to Content.Shared.StatusEffectNew.SharedStatusEffectsSystem is required")]
public bool TryRemoveStatusEffect(EntityUid uid, string key, public bool TryRemoveStatusEffect(EntityUid uid, string key,
StatusEffectsComponent? status = null, bool remComp = true) StatusEffectsComponent? status = null, bool remComp = true)
{ {
@@ -298,6 +304,7 @@ namespace Content.Shared.StatusEffect
/// <param name="uid">The entity to remove effects from.</param> /// <param name="uid">The entity to remove effects from.</param>
/// <param name="status">The status effects component to change, if you already have it.</param> /// <param name="status">The status effects component to change, if you already have it.</param>
/// <returns>False if any status effects failed to be removed, true if they all did.</returns> /// <returns>False if any status effects failed to be removed, true if they all did.</returns>
[Obsolete("Migration to Content.Shared.StatusEffectNew.SharedStatusEffectsSystem is required")]
public bool TryRemoveAllStatusEffects(EntityUid uid, public bool TryRemoveAllStatusEffects(EntityUid uid,
StatusEffectsComponent? status = null) StatusEffectsComponent? status = null)
{ {
@@ -321,6 +328,7 @@ namespace Content.Shared.StatusEffect
/// <param name="uid">The entity to check on.</param> /// <param name="uid">The entity to check on.</param>
/// <param name="key">The status effect ID to check for</param> /// <param name="key">The status effect ID to check for</param>
/// <param name="status">The status effect component, should you already have it.</param> /// <param name="status">The status effect component, should you already have it.</param>
[Obsolete("Migration to Content.Shared.StatusEffectNew.SharedStatusEffectsSystem is required")]
public bool HasStatusEffect(EntityUid uid, string key, public bool HasStatusEffect(EntityUid uid, string key,
StatusEffectsComponent? status = null) StatusEffectsComponent? status = null)
{ {
@@ -338,6 +346,7 @@ namespace Content.Shared.StatusEffect
/// <param name="uid">The entity to check on.</param> /// <param name="uid">The entity to check on.</param>
/// <param name="key">The status effect ID to check for</param> /// <param name="key">The status effect ID to check for</param>
/// <param name="status">The status effect component, should you already have it.</param> /// <param name="status">The status effect component, should you already have it.</param>
[Obsolete("Migration to Content.Shared.StatusEffectNew.SharedStatusEffectsSystem is required")]
public bool CanApplyEffect(EntityUid uid, string key, StatusEffectsComponent? status = null) 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 // 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
/// <param name="key">The status effect to add time to.</param> /// <param name="key">The status effect to add time to.</param>
/// <param name="time">The amount of time to add.</param> /// <param name="time">The amount of time to add.</param>
/// <param name="status">The status effect component, should you already have it.</param> /// <param name="status">The status effect component, should you already have it.</param>
[Obsolete("Migration to Content.Shared.StatusEffectNew.SharedStatusEffectsSystem is required")]
public bool TryAddTime(EntityUid uid, string key, TimeSpan time, public bool TryAddTime(EntityUid uid, string key, TimeSpan time,
StatusEffectsComponent? status = null) StatusEffectsComponent? status = null)
{ {
@@ -395,6 +405,7 @@ namespace Content.Shared.StatusEffect
/// <param name="key">The status effect to remove time from.</param> /// <param name="key">The status effect to remove time from.</param>
/// <param name="time">The amount of time to add.</param> /// <param name="time">The amount of time to add.</param>
/// <param name="status">The status effect component, should you already have it.</param> /// <param name="status">The status effect component, should you already have it.</param>
[Obsolete("Migration to Content.Shared.StatusEffectNew.SharedStatusEffectsSystem is required")]
public bool TryRemoveTime(EntityUid uid, string key, TimeSpan time, public bool TryRemoveTime(EntityUid uid, string key, TimeSpan time,
StatusEffectsComponent? status = null) StatusEffectsComponent? status = null)
{ {
@@ -430,6 +441,7 @@ namespace Content.Shared.StatusEffect
/// <remarks> /// <remarks>
/// Not used internally; just sets it itself. /// Not used internally; just sets it itself.
/// </remarks> /// </remarks>
[Obsolete("Migration to Content.Shared.StatusEffectNew.SharedStatusEffectsSystem is required")]
public bool TrySetTime(EntityUid uid, string key, TimeSpan time, public bool TrySetTime(EntityUid uid, string key, TimeSpan time,
StatusEffectsComponent? status = null) StatusEffectsComponent? status = null)
{ {
@@ -453,6 +465,7 @@ namespace Content.Shared.StatusEffect
/// <param name="time">Out var for the time, if it exists.</param> /// <param name="time">Out var for the time, if it exists.</param>
/// <param name="status">The status effects component to use, if any.</param> /// <param name="status">The status effects component to use, if any.</param>
/// <returns>False if the status effect was not active, true otherwise.</returns> /// <returns>False if the status effect was not active, true otherwise.</returns>
[Obsolete("Migration to Content.Shared.StatusEffectNew.SharedStatusEffectsSystem is required")]
public bool TryGetTime(EntityUid uid, string key, public bool TryGetTime(EntityUid uid, string key,
[NotNullWhen(true)] out (TimeSpan, TimeSpan)? time, [NotNullWhen(true)] out (TimeSpan, TimeSpan)? time,
StatusEffectsComponent? status = null) StatusEffectsComponent? status = null)
@@ -468,12 +481,6 @@ namespace Content.Shared.StatusEffect
} }
} }
/// <summary>
/// Raised on an entity before a status effect is added to determine if adding it should be cancelled.
/// </summary>
[ByRefEvent]
public record struct BeforeStatusEffectAddedEvent(string Key, bool Cancelled=false);
public readonly struct StatusEffectAddedEvent public readonly struct StatusEffectAddedEvent
{ {
public readonly EntityUid Uid; public readonly EntityUid Uid;

View File

@@ -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;
/// <summary>
/// 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.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause]
[Access(typeof(SharedStatusEffectsSystem))]
[EntityCategory("StatusEffects")]
public sealed partial class StatusEffectComponent : Component
{
/// <summary>
/// The entity that this status effect is applied to.
/// </summary>
[DataField, AutoNetworkedField]
public EntityUid? AppliedTo;
/// <summary>
/// Status effect indication for the player. If Null, no Alert will be displayed.
/// </summary>
[DataField]
public ProtoId<AlertPrototype>? Alert;
/// <summary>
/// When this effect will end. If Null, the effect lasts indefinitely.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField, AutoNetworkedField]
public TimeSpan? EndEffectTime;
/// <summary>
/// Whitelist, by which it is determined whether this status effect can be imposed on a particular entity.
/// </summary>
[DataField]
public EntityWhitelist? Whitelist;
/// <summary>
/// Blacklist, by which it is determined whether this status effect can be imposed on a particular entity.
/// </summary>
[DataField]
public EntityWhitelist? Blacklist;
}

View File

@@ -0,0 +1,23 @@
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
namespace Content.Shared.StatusEffectNew.Components;
/// <summary>
/// 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.
/// </summary>
[RegisterComponent, NetworkedComponent]
[Access(typeof(SharedStatusEffectsSystem))]
public sealed partial class StatusEffectContainerComponent : Component
{
[DataField]
public HashSet<EntityUid> ActiveStatusEffects = new();
}
[Serializable, NetSerializable]
public sealed class StatusEffectContainerComponentState(HashSet<NetEntity> activeStatusEffects) : ComponentState
{
public readonly HashSet<NetEntity> ActiveStatusEffects = activeStatusEffects;
}

View File

@@ -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;
/// <summary>
/// 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.
/// </summary>
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<StatusEffectContainerComponent> _containerQuery;
private EntityQuery<StatusEffectComponent> _effectQuery;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<StatusEffectComponent, StatusEffectAppliedEvent>(OnStatusEffectApplied);
SubscribeLocalEvent<StatusEffectComponent, StatusEffectRemovedEvent>(OnStatusEffectRemoved);
SubscribeLocalEvent<StatusEffectContainerComponent, LocalPlayerAttachedEvent>(OnStatusEffectContainerAttached);
SubscribeLocalEvent<StatusEffectContainerComponent, LocalPlayerDetachedEvent>(OnStatusEffectContainerDetached);
SubscribeLocalEvent<StatusEffectContainerComponent, ComponentGetState>(OnGetState);
_containerQuery = GetEntityQuery<StatusEffectContainerComponent>();
_effectQuery = GetEntityQuery<StatusEffectComponent>();
}
private void OnGetState(Entity<StatusEffectContainerComponent> ent, ref ComponentGetState args)
{
args.State = new StatusEffectContainerComponentState(GetNetEntitySet(ent.Comp.ActiveStatusEffects));
}
private void OnStatusEffectContainerAttached(Entity<StatusEffectContainerComponent> ent, ref LocalPlayerAttachedEvent args)
{
foreach (var effect in ent.Comp.ActiveStatusEffects)
{
var ev = new StatusEffectPlayerAttachedEvent(ent);
RaiseLocalEvent(effect, ref ev);
}
}
private void OnStatusEffectContainerDetached(Entity<StatusEffectContainerComponent> 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<StatusEffectComponent>();
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<StatusEffectComponent> 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<StatusEffectComponent> 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<StatusEffectComponent>(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;
}
}
/// <summary>
/// Calls on effect entity, when a status effect is applied.
/// </summary>
[ByRefEvent]
public readonly record struct StatusEffectAppliedEvent(EntityUid Target);
/// <summary>
/// Calls on effect entity, when a status effect is removed.
/// </summary>
[ByRefEvent]
public readonly record struct StatusEffectRemovedEvent(EntityUid Target);
/// <summary>
/// Called on a status effect entity inside <see cref="StatusEffectContainerComponent"/>
/// after a player has been <see cref="LocalPlayerAttachedEvent"/> to this container entity.
/// </summary>
[ByRefEvent]
public readonly record struct StatusEffectPlayerAttachedEvent(EntityUid Target);
/// <summary>
/// Called on a status effect entity inside <see cref="StatusEffectContainerComponent"/>
/// after a player has been <see cref="LocalPlayerDetachedEvent"/> to this container entity.
/// </summary>
[ByRefEvent]
public readonly record struct StatusEffectPlayerDetachedEvent(EntityUid Target);
/// <summary>
/// Raised on an entity before a status effect is added to determine if adding it should be cancelled.
/// </summary>
[ByRefEvent]
public record struct BeforeStatusEffectAddedEvent(EntProtoId Effect, bool Cancelled = false);

View File

@@ -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
{
/// <summary>
/// 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.
/// </summary>
/// <param name="target">The target entity to which the effect should be added.</param>
/// <param name="effectProto">ProtoId of the status effect entity. Make sure it has StatusEffectComponent on it.</param>
/// <param name="duration">Duration of status effect. Leave null and the effect will be permanent until it is removed using <c>TryRemoveStatusEffect</c>.</param>
/// <param name="resetCooldown">
/// 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.
/// </param>
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<StatusEffectContainerComponent>(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;
}
/// <summary>
/// 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.
/// </summary>
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;
}
/// <summary>
/// Checks whether the specified entity is under a specific status effect.
/// </summary>
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;
}
/// <summary>
/// Attempting to retrieve the EntityUid of a status effect from an entity.
/// </summary>
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;
}
/// <summary>
/// Attempting to retrieve the time of a status effect from an entity.
/// </summary>
/// <param name="uid">The target entity on which the effect is applied.</param>
/// <param name="effectProto">The prototype ID of the status effect to retrieve.</param>
/// <param name="time">The output tuple containing the effect entity and its remaining time.</param>
/// <param name="container">Optional. The status effect container component of the entity.</param>
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;
}
/// <summary>
/// Attempts to edit the remaining time for a status effect on an entity.
/// </summary>
/// <param name="uid">The target entity on which the effect is applied.</param>
/// <param name="effectProto">The prototype ID of the status effect to modify.</param>
/// <param name="time">
/// The time adjustment to apply to the status effect. Positive values extend the duration,
/// while negative values reduce it.
/// </param>
/// <returns> True if duration was edited successfully, false otherwise.</returns>
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;
}
/// <summary>
/// Attempts to set the remaining time for a status effect on an entity.
/// </summary>
/// <param name="uid">The target entity on which the effect is applied.</param>
/// <param name="effectProto">The prototype ID of the status effect to modify.</param>
/// <param name="time">The new duration for the status effect.</param>
/// <returns> True if duration was set successfully, false otherwise.</returns>
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;
}
/// <summary>
/// Checks if the specified component is present on any of the entity's status effects.
/// </summary>
public bool HasEffectComp<T>(EntityUid? target) where T : IComponent
{
if (!_containerQuery.TryComp(target, out var container))
return false;
foreach (var effect in container.ActiveStatusEffects)
{
if (HasComp<T>(effect))
return true;
}
return false;
}
/// <summary>
/// Returns all status effects that have the specified component.
/// </summary>
public bool TryEffectsWithComp<T>(EntityUid? target, [NotNullWhen(true)] out HashSet<Entity<T, StatusEffectComponent>>? effects) where T : IComponent
{
effects = null;
if (!_containerQuery.TryComp(target, out var container))
return false;
foreach (var effect in container.ActiveStatusEffects)
{
if (!TryComp<StatusEffectComponent>(effect, out var statusComp))
continue;
if (TryComp<T>(effect, out var comp))
{
effects ??= [];
effects.Add((effect, comp, statusComp));
}
}
return effects != null;
}
}

View File

@@ -4,5 +4,6 @@ entity-category-name-objectives = Objectives
entity-category-name-roles = Mind Roles entity-category-name-roles = Mind Roles
entity-category-name-mapping = Mapping entity-category-name-mapping = Mapping
entity-category-name-donotmap = Do not map entity-category-name-donotmap = Do not map
entity-category-name-status-effects = Status Effects
entity-category-suffix-donotmap = DO NOT MAP entity-category-suffix-donotmap = DO NOT MAP

View File

@@ -25,7 +25,6 @@
- Electrocution - Electrocution
- TemporaryBlindness - TemporaryBlindness
- RadiationProtection - RadiationProtection
- Drowsiness
- Adrenaline - Adrenaline
- type: StandingState - type: StandingState
- type: Tag - type: Tag

View File

@@ -22,12 +22,10 @@
- SlowedDown - SlowedDown
- Stutter - Stutter
- Electrocution - Electrocution
- ForcedSleep
- TemporaryBlindness - TemporaryBlindness
- Pacified - Pacified
- Flashed - Flashed
- RadiationProtection - RadiationProtection
- Drowsiness
- Adrenaline - Adrenaline
- type: Buckle - type: Buckle
- type: StandingState - type: StandingState
@@ -100,13 +98,11 @@
- SlowedDown - SlowedDown
- Stutter - Stutter
- Electrocution - Electrocution
- ForcedSleep
- TemporaryBlindness - TemporaryBlindness
- Pacified - Pacified
- StaminaModifier - StaminaModifier
- Flashed - Flashed
- RadiationProtection - RadiationProtection
- Drowsiness
- Adrenaline - Adrenaline
- type: Bloodstream - type: Bloodstream
bloodMaxVolume: 150 bloodMaxVolume: 150

View File

@@ -103,11 +103,9 @@
- SlowedDown - SlowedDown
- Stutter - Stutter
- Electrocution - Electrocution
- ForcedSleep
- TemporaryBlindness - TemporaryBlindness
- Pacified - Pacified
- RadiationProtection - RadiationProtection
- Drowsiness
- Adrenaline - Adrenaline
- type: Temperature - type: Temperature
heatDamageThreshold: 800 heatDamageThreshold: 800

View File

@@ -130,13 +130,11 @@
- RatvarianLanguage - RatvarianLanguage
- PressureImmunity - PressureImmunity
- Muted - Muted
- ForcedSleep
- TemporaryBlindness - TemporaryBlindness
- Pacified - Pacified
- StaminaModifier - StaminaModifier
- Flashed - Flashed
- RadiationProtection - RadiationProtection
- Drowsiness
- Adrenaline - Adrenaline
- type: Body - type: Body
prototype: Human prototype: Human

View File

@@ -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

View File

@@ -27,3 +27,8 @@
id: DoNotMap id: DoNotMap
name: entity-category-name-donotmap name: entity-category-name-donotmap
suffix: entity-category-suffix-donotmap suffix: entity-category-suffix-donotmap
- type: entityCategory
id: StatusEffects
name: entity-category-name-status-effects
hideSpawnMenu: true

View File

@@ -2018,9 +2018,9 @@
- !type:AdjustReagent - !type:AdjustReagent
reagent: Theobromine reagent: Theobromine
amount: 0.05 amount: 0.05
- !type:GenericStatusEffect - !type:ModifyStatusEffect
key: Drowsiness effectProto: StatusEffectDrowsiness
time: 1.0 time: 1
type: Remove type: Remove
fizziness: 0.25 fizziness: 0.25
@@ -2049,9 +2049,9 @@
- !type:AdjustReagent - !type:AdjustReagent
reagent: Theobromine reagent: Theobromine
amount: 0.05 amount: 0.05
- !type:GenericStatusEffect - !type:ModifyStatusEffect
key: Drowsiness effectProto: StatusEffectDrowsiness
time: 1.0 time: 1
type: Remove type: Remove
fizziness: 0.15 fizziness: 0.15
@@ -2080,9 +2080,9 @@
- !type:AdjustReagent - !type:AdjustReagent
reagent: Theobromine reagent: Theobromine
amount: 0.05 amount: 0.05
- !type:GenericStatusEffect - !type:ModifyStatusEffect
key: Drowsiness effectProto: StatusEffectDrowsiness
time: 1.0 time: 1
type: Remove type: Remove
fizziness: 0.15 fizziness: 0.15
@@ -2111,9 +2111,9 @@
- !type:AdjustReagent - !type:AdjustReagent
reagent: Theobromine reagent: Theobromine
amount: 0.05 amount: 0.05
- !type:GenericStatusEffect - !type:ModifyStatusEffect
key: Drowsiness effectProto: StatusEffectDrowsiness
time: 1.0 time: 1
type: Remove type: Remove
fizziness: 0.25 fizziness: 0.25
@@ -2142,9 +2142,9 @@
- !type:AdjustReagent - !type:AdjustReagent
reagent: Theobromine reagent: Theobromine
amount: 0.05 amount: 0.05
- !type:GenericStatusEffect - !type:ModifyStatusEffect
key: Drowsiness effectProto: StatusEffectDrowsiness
time: 1.0 time: 1
type: Remove type: Remove
fizziness: 0.15 fizziness: 0.15
@@ -2173,9 +2173,9 @@
- !type:AdjustReagent - !type:AdjustReagent
reagent: Theobromine reagent: Theobromine
amount: 0.05 amount: 0.05
- !type:GenericStatusEffect - !type:ModifyStatusEffect
key: Drowsiness effectProto: StatusEffectDrowsiness
time: 1.0 time: 1
type: Remove type: Remove
fizziness: 0.25 fizziness: 0.25

View File

@@ -12,9 +12,9 @@
effects: effects:
- !type:SatiateThirst - !type:SatiateThirst
factor: 2 factor: 2
- !type:GenericStatusEffect - !type:ModifyStatusEffect
key: Drowsiness effectProto: StatusEffectDrowsiness
time: 2.0 time: 2
type: Remove type: Remove
- !type:AdjustReagent - !type:AdjustReagent
reagent: Theobromine reagent: Theobromine
@@ -105,9 +105,9 @@
effects: effects:
- !type:SatiateThirst - !type:SatiateThirst
factor: 2 factor: 2
- !type:GenericStatusEffect - !type:ModifyStatusEffect
key: Drowsiness effectProto: StatusEffectDrowsiness
time: 2.0 time: 2
type: Remove type: Remove
- type: reagent - type: reagent
@@ -161,9 +161,9 @@
effects: effects:
- !type:SatiateThirst - !type:SatiateThirst
factor: 2 factor: 2
- !type:GenericStatusEffect - !type:ModifyStatusEffect
key: Drowsiness effectProto: StatusEffectDrowsiness
time: 2.0 time: 2
type: Remove type: Remove
- type: reagent - type: reagent
@@ -342,9 +342,9 @@
effects: effects:
- !type:SatiateThirst - !type:SatiateThirst
factor: 6 factor: 6
- !type:GenericStatusEffect - !type:ModifyStatusEffect
key: Drowsiness effectProto: StatusEffectDrowsiness
time: 3.0 time: 3
type: Remove type: Remove
Poison: Poison:
effects: effects:
@@ -383,9 +383,9 @@
effects: effects:
- !type:SatiateThirst - !type:SatiateThirst
factor: 2 factor: 2
- !type:GenericStatusEffect - !type:ModifyStatusEffect
key: Drowsiness effectProto: StatusEffectDrowsiness
time: 2.0 time: 2
type: Remove type: Remove
- type: reagent - type: reagent

View File

@@ -18,9 +18,9 @@
effects: effects:
- !type:SatiateThirst - !type:SatiateThirst
factor: 2 factor: 2
- !type:GenericStatusEffect - !type:ModifyStatusEffect
key: Drowsiness effectProto: StatusEffectDrowsiness
time: 1.0 time: 1
type: Remove type: Remove
- type: reagent - type: reagent
@@ -80,9 +80,9 @@
effects: effects:
- !type:SatiateThirst - !type:SatiateThirst
factor: 2 factor: 2
- !type:GenericStatusEffect - !type:ModifyStatusEffect
key: Drowsiness effectProto: StatusEffectDrowsiness
time: 2.0 time: 2
type: Remove type: Remove
- !type:AdjustReagent - !type:AdjustReagent
reagent: Theobromine reagent: Theobromine

View File

@@ -360,7 +360,7 @@
shouldHave: false shouldHave: false
walkSpeedModifier: 0.65 walkSpeedModifier: 0.65
sprintSpeedModifier: 0.65 sprintSpeedModifier: 0.65
- !type:GenericStatusEffect - !type:ModifyStatusEffect
conditions: conditions:
- !type:ReagentThreshold - !type:ReagentThreshold
reagent: NitrousOxide reagent: NitrousOxide
@@ -368,8 +368,7 @@
- !type:OrganType - !type:OrganType
type: Slime type: Slime
shouldHave: false shouldHave: false
key: ForcedSleep effectProto: StatusEffectForcedSleeping
component: ForcedSleeping
time: 3 time: 3
type: Add type: Add
- !type:HealthChange - !type:HealthChange

View File

@@ -83,9 +83,8 @@
key: Jitter key: Jitter
time: 3.0 time: 3.0
type: Remove type: Remove
- !type:GenericStatusEffect - !type:ModifyStatusEffect
key: Drowsiness effectProto: StatusEffectDrowsiness
component: Drowsiness
time: 1.5 time: 1.5
type: Add type: Add
refresh: false refresh: false
@@ -923,8 +922,8 @@
- !type:GenericStatusEffect - !type:GenericStatusEffect
key: Stutter key: Stutter
component: StutteringAccent component: StutteringAccent
- !type:GenericStatusEffect - !type:ModifyStatusEffect
key: Drowsiness effectProto: StatusEffectDrowsiness
time: 10 time: 10
type: Remove type: Remove
- !type:ResetNarcolepsy - !type:ResetNarcolepsy
@@ -948,8 +947,8 @@
metabolisms: metabolisms:
Medicine: Medicine:
effects: effects:
- !type:GenericStatusEffect - !type:ModifyStatusEffect
key: Drowsiness effectProto: StatusEffectDrowsiness
time: 10 time: 10
type: Remove type: Remove
- !type:ResetNarcolepsy - !type:ResetNarcolepsy
@@ -1390,9 +1389,8 @@
emote: Yawn emote: Yawn
showInChat: true showInChat: true
probability: 0.1 probability: 0.1
- !type:GenericStatusEffect - !type:ModifyStatusEffect
key: Drowsiness effectProto: StatusEffectDrowsiness
component: Drowsiness
time: 4 time: 4
type: Add type: Add
refresh: false refresh: false

View File

@@ -40,12 +40,12 @@
key: KnockedDown key: KnockedDown
time: 3 time: 3
type: Remove type: Remove
- !type:GenericStatusEffect - !type:ModifyStatusEffect
conditions: conditions:
- !type:ReagentThreshold - !type:ReagentThreshold
reagent: Haloperidol reagent: Haloperidol
max: 0.01 max: 0.01
key: Drowsiness effectProto: StatusEffectDrowsiness
time: 10 time: 10
type: Remove type: Remove
Medicine: Medicine:
@@ -88,12 +88,12 @@
key: KnockedDown key: KnockedDown
time: 1 time: 1
type: Remove type: Remove
- !type:GenericStatusEffect - !type:ModifyStatusEffect
conditions: conditions:
- !type:ReagentThreshold - !type:ReagentThreshold
reagent: Haloperidol reagent: Haloperidol
max: 0.01 max: 0.01
key: Drowsiness effectProto: StatusEffectDrowsiness
time: 10 time: 10
type: Remove type: Remove
- !type:PopupMessage - !type:PopupMessage
@@ -152,16 +152,16 @@
component: StaminaModifier component: StaminaModifier
time: 3 time: 3
type: Add type: Add
- !type:GenericStatusEffect - !type:ModifyStatusEffect
key: ForcedSleep effectProto: StatusEffectForcedSleeping
time: 3 time: 3
type: Remove type: Remove
- !type:GenericStatusEffect - !type:ModifyStatusEffect
conditions: conditions:
- !type:ReagentThreshold - !type:ReagentThreshold
reagent: Haloperidol reagent: Haloperidol
max: 0.01 max: 0.01
key: Drowsiness effectProto: StatusEffectDrowsiness
time: 10 time: 10
type: Remove type: Remove
Medicine: Medicine:
@@ -296,14 +296,13 @@
metabolisms: metabolisms:
Narcotic: Narcotic:
effects: effects:
- !type:GenericStatusEffect - !type:ModifyStatusEffect
conditions: conditions:
- !type:ReagentThreshold - !type:ReagentThreshold
reagent: Nocturine reagent: Nocturine
min: 8 min: 8
key: ForcedSleep effectProto: StatusEffectForcedSleeping
component: ForcedSleeping time: 3
refresh: false
type: Add type: Add
- type: reagent - type: reagent

View File

@@ -63,9 +63,8 @@
- !type:MovespeedModifier - !type:MovespeedModifier
walkSpeedModifier: 0.65 walkSpeedModifier: 0.65
sprintSpeedModifier: 0.65 sprintSpeedModifier: 0.65
- !type:GenericStatusEffect - !type:ModifyStatusEffect
key: Drowsiness effectProto: StatusEffectDrowsiness
component: Drowsiness
time: 4 time: 4
type: Add type: Add
refresh: false refresh: false

View File

@@ -1,6 +1,9 @@
# Status effect prototypes. # Status effect prototypes.
# Holds no actual logic, just some basic data about the effect. # 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 - type: statusEffect
id: Stun id: Stun
alert: Stun alert: Stun
@@ -45,9 +48,6 @@
id: Corporeal id: Corporeal
alert: Corporeal alert: Corporeal
- type: statusEffect
id: ForcedSleep #I.e., they will not wake on damage or similar
- type: statusEffect - type: statusEffect
id: TemporaryBlindness id: TemporaryBlindness
@@ -66,9 +66,6 @@
- type: statusEffect - type: statusEffect
id: RadiationProtection id: RadiationProtection
- type: statusEffect
id: Drowsiness #blurs your vision and makes you randomly fall asleep
- type: statusEffect - type: statusEffect
id: Adrenaline id: Adrenaline
alert: Adrenaline alert: Adrenaline