diff --git a/Content.Shared/Bed/Sleep/SleepingSystem.cs b/Content.Shared/Bed/Sleep/SleepingSystem.cs index 141a130053..eca6a8befa 100644 --- a/Content.Shared/Bed/Sleep/SleepingSystem.cs +++ b/Content.Shared/Bed/Sleep/SleepingSystem.cs @@ -14,6 +14,7 @@ using Content.Shared.Mobs; using Content.Shared.Mobs.Components; using Content.Shared.Pointing; using Content.Shared.Popups; +using Content.Shared.Rejuvenate; using Content.Shared.Slippery; using Content.Shared.Sound; using Content.Shared.Sound.Components; @@ -57,7 +58,7 @@ public sealed partial class SleepingSystem : EntitySystem SubscribeLocalEvent(OnDamageChanged); SubscribeLocalEvent(OnZombified); SubscribeLocalEvent(OnMobStateChanged); - SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnCompInit); SubscribeLocalEvent(OnSpeakAttempt); SubscribeLocalEvent(OnSeeAttempt); SubscribeLocalEvent(OnPointAttempt); @@ -68,6 +69,7 @@ public sealed partial class SleepingSystem : EntitySystem SubscribeLocalEvent(OnInteractHand); SubscribeLocalEvent(OnStunEndAttempt); SubscribeLocalEvent(OnStandUpAttempt); + SubscribeLocalEvent(OnRejuvenate); SubscribeLocalEvent(OnStatusEffectApplied); SubscribeLocalEvent(OnUnbuckleAttempt); @@ -133,7 +135,7 @@ public sealed partial class SleepingSystem : EntitySystem RemComp(ent); } - private void OnMapInit(Entity ent, ref MapInitEvent args) + private void OnCompInit(Entity ent, ref ComponentInit args) { var ev = new SleepStateChangedEvent(true); RaiseLocalEvent(ent, ref ev); @@ -185,6 +187,11 @@ public sealed partial class SleepingSystem : EntitySystem args.Cancelled = true; } + private void OnRejuvenate(Entity ent, ref RejuvenateEvent args) + { + TryWaking((ent.Owner, ent.Comp), true); + } + private void OnExamined(Entity ent, ref ExaminedEvent args) { if (args.IsInDetailsRange) diff --git a/Content.Shared/EntityEffects/Effects/StatusEffects/ModifyStatusEffect.cs b/Content.Shared/EntityEffects/Effects/StatusEffects/ModifyStatusEffect.cs index a232d34925..cb010b648c 100644 --- a/Content.Shared/EntityEffects/Effects/StatusEffects/ModifyStatusEffect.cs +++ b/Content.Shared/EntityEffects/Effects/StatusEffects/ModifyStatusEffect.cs @@ -19,6 +19,12 @@ public sealed partial class ModifyStatusEffect : EntityEffect [DataField] public float Time = 2.0f; + /// + /// Delay before the effect starts. If another effect is added with a shorter delay, it takes precedence. + /// + [DataField] + public float Delay = 0f; + /// /// true - refresh status effect time (update to greater value), false - accumulate status effect time. /// @@ -45,22 +51,30 @@ public sealed partial class ModifyStatusEffect : EntityEffect { case StatusEffectMetabolismType.Add: if (Refresh) - statusSys.TryUpdateStatusEffectDuration(args.TargetEntity, EffectProto, duration); + statusSys.TryUpdateStatusEffectDuration(args.TargetEntity, EffectProto, duration, Delay > 0 ? TimeSpan.FromSeconds(Delay) : null); else - statusSys.TryAddStatusEffectDuration(args.TargetEntity, EffectProto, duration); + statusSys.TryAddStatusEffectDuration(args.TargetEntity, EffectProto, duration, Delay > 0 ? TimeSpan.FromSeconds(Delay) : null); break; case StatusEffectMetabolismType.Remove: statusSys.TryAddTime(args.TargetEntity, EffectProto, -duration); break; case StatusEffectMetabolismType.Set: - statusSys.TrySetStatusEffectDuration(args.TargetEntity, EffectProto, duration); + statusSys.TrySetStatusEffectDuration(args.TargetEntity, EffectProto, duration, TimeSpan.FromSeconds(Delay)); break; } } /// - protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) - => Loc.GetString( + protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => + Delay > 0 + ? Loc.GetString( + "reagent-effect-guidebook-status-effect-delay", + ("chance", Probability), + ("type", Type), + ("time", Time), + ("key", prototype.Index(EffectProto).Name), + ("delay", Delay)) + : Loc.GetString( "reagent-effect-guidebook-status-effect", ("chance", Probability), ("type", Type), diff --git a/Content.Shared/StatusEffectNew/Components/StatusEffectComponent.cs b/Content.Shared/StatusEffectNew/Components/StatusEffectComponent.cs index 67ff8b3e61..27764b3aee 100644 --- a/Content.Shared/StatusEffectNew/Components/StatusEffectComponent.cs +++ b/Content.Shared/StatusEffectNew/Components/StatusEffectComponent.cs @@ -9,7 +9,7 @@ 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] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(fieldDeltas: true), AutoGenerateComponentPause] [Access(typeof(StatusEffectsSystem))] [EntityCategory("StatusEffects")] public sealed partial class StatusEffectComponent : Component @@ -20,12 +20,24 @@ public sealed partial class StatusEffectComponent : Component [DataField, AutoNetworkedField] public EntityUid? AppliedTo; + /// + /// When this effect will start. Set to Timespan.Zero to start the effect immediately. + /// + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField, AutoNetworkedField] + public TimeSpan StartEffectTime; + /// /// When this effect will end. If Null, the effect lasts indefinitely. /// [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField, AutoNetworkedField] public TimeSpan? EndEffectTime; + /// + /// If true, this status effect has been applied. Used to ensure that only fires once. + /// + [DataField, AutoNetworkedField] + public bool Applied; + /// /// Whitelist, by which it is determined whether this status effect can be imposed on a particular entity. /// diff --git a/Content.Shared/StatusEffectNew/StatusEffectSystem.API.cs b/Content.Shared/StatusEffectNew/StatusEffectSystem.API.cs index 56636c9601..905ad98c6c 100644 --- a/Content.Shared/StatusEffectNew/StatusEffectSystem.API.cs +++ b/Content.Shared/StatusEffectNew/StatusEffectSystem.API.cs @@ -13,13 +13,15 @@ public sealed partial class StatusEffectsSystem /// 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. + /// The delay of the effect. If a start time already exists, the closest time takes precedence. Leave null for the effect to be instant. /// The EntityUid of the status effect we have just created or null if it doesn't exist. /// True if effect exists and its duration is set properly, false in case effect cannot be applied. public bool TryAddStatusEffectDuration( EntityUid target, EntProtoId effectProto, [NotNullWhen(true)] out EntityUid? statusEffect, - TimeSpan duration + TimeSpan duration, + TimeSpan? delay = null ) { if (duration == TimeSpan.Zero) @@ -30,18 +32,19 @@ public sealed partial class StatusEffectsSystem // We check to make sure time is greater than zero here because sometimes you want to use TryAddStatusEffect to remove duration instead... if (!TryGetStatusEffect(target, effectProto, out statusEffect)) - return TryAddStatusEffect(target, effectProto, out statusEffect, duration); + return TryAddStatusEffect(target, effectProto, out statusEffect, duration, delay); AddStatusEffectTime(statusEffect.Value, duration); + UpdateStatusEffectDelay(statusEffect.Value, delay); return true; } - /// - public bool TryAddStatusEffectDuration(EntityUid target, EntProtoId effectProto, TimeSpan duration) + /// + public bool TryAddStatusEffectDuration(EntityUid target, EntProtoId effectProto, TimeSpan duration, TimeSpan? delay = null) { - return TryAddStatusEffectDuration(target, effectProto, out _, duration); + return TryAddStatusEffectDuration(target, effectProto, out _, duration, delay); } /// @@ -51,13 +54,15 @@ public sealed partial class StatusEffectsSystem /// 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. + /// The delay of the effect. If a start time already exists, the closest time takes precedence. Leave null for the effect to be instant. /// The EntityUid of the status effect we have just created or null if it doesn't exist. /// True if effect exists and its duration is set properly, false in case effect cannot be applied. public bool TrySetStatusEffectDuration( EntityUid target, EntProtoId effectProto, [NotNullWhen(true)] out EntityUid? statusEffect, - TimeSpan? duration = null + TimeSpan? duration = null, + TimeSpan? delay = null ) { if (duration <= TimeSpan.Zero) @@ -67,17 +72,22 @@ public sealed partial class StatusEffectsSystem } if (!TryGetStatusEffect(target, effectProto, out statusEffect)) - return TryAddStatusEffect(target, effectProto, out statusEffect, duration); + return TryAddStatusEffect(target, effectProto, out statusEffect, duration, delay); - SetStatusEffectEndTime(statusEffect.Value, duration); + if (!_effectQuery.TryComp(statusEffect, out var statusEffectComponent)) + return false; + + var endTime = delay == null || statusEffectComponent.Applied ? _timing.CurTime + duration : _timing.CurTime + delay + duration; + SetStatusEffectEndTime(statusEffect.Value, endTime); + UpdateStatusEffectDelay(statusEffect.Value, delay); return true; } - /// - public bool TrySetStatusEffectDuration(EntityUid target, EntProtoId effectProto, TimeSpan? duration = null) + /// + public bool TrySetStatusEffectDuration(EntityUid target, EntProtoId effectProto, TimeSpan? duration = null, TimeSpan? delay = null) { - return TrySetStatusEffectDuration(target, effectProto, out _, duration); + return TrySetStatusEffectDuration(target, effectProto, out _, duration, delay); } /// @@ -87,13 +97,15 @@ public sealed partial class StatusEffectsSystem /// 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. + /// The delay of the effect. If a start time already exists, the closest time takes precedence. Leave null for the effect to be instant. /// The EntityUid of the status effect we have just created or null if it doesn't exist. /// True if effect exists and its duration is set properly, false in case effect cannot be applied. public bool TryUpdateStatusEffectDuration( EntityUid target, EntProtoId effectProto, [NotNullWhen(true)] out EntityUid? statusEffect, - TimeSpan? duration = null + TimeSpan? duration = null, + TimeSpan? delay = null ) { if (duration <= TimeSpan.Zero) @@ -103,17 +115,22 @@ public sealed partial class StatusEffectsSystem } if (!TryGetStatusEffect(target, effectProto, out statusEffect)) - return TryAddStatusEffect(target, effectProto, out statusEffect, duration); + return TryAddStatusEffect(target, effectProto, out statusEffect, duration, delay); - UpdateStatusEffectTime(statusEffect.Value, duration); + if (!_effectQuery.TryComp(statusEffect, out var statusEffectComponent)) + return false; + + var endTime = delay == null || statusEffectComponent.Applied ? duration : delay + duration; + UpdateStatusEffectTime(statusEffect.Value, endTime); + UpdateStatusEffectDelay(statusEffect.Value, delay); return true; } - /// - public bool TryUpdateStatusEffectDuration(EntityUid target, EntProtoId effectProto, TimeSpan? duration = null) + /// + public bool TryUpdateStatusEffectDuration(EntityUid target, EntProtoId effectProto, TimeSpan? duration = null, TimeSpan? delay = null) { - return TryUpdateStatusEffectDuration(target, effectProto, out _, duration); + return TryUpdateStatusEffectDuration(target, effectProto, out _, duration, delay); } /// @@ -193,7 +210,7 @@ public sealed partial class StatusEffectsSystem public bool TryGetTime( EntityUid uid, EntProtoId effectProto, - out (EntityUid EffectEnt, TimeSpan? EndEffectTime) time, + out (EntityUid EffectEnt, TimeSpan? EndEffectTime, TimeSpan? StartEffectTime) time, StatusEffectContainerComponent? container = null ) { @@ -209,7 +226,7 @@ public sealed partial class StatusEffectsSystem if (!_effectQuery.TryComp(effect, out var effectComp)) return false; - time = (effect, effectComp.EndEffectTime); + time = (effect, effectComp.EndEffectTime, effectComp.StartEffectTime); return true; } } diff --git a/Content.Shared/StatusEffectNew/StatusEffectsSystem.cs b/Content.Shared/StatusEffectNew/StatusEffectsSystem.cs index 446b3fd3b1..966878b4e3 100644 --- a/Content.Shared/StatusEffectNew/StatusEffectsSystem.cs +++ b/Content.Shared/StatusEffectNew/StatusEffectsSystem.cs @@ -46,6 +46,8 @@ public sealed partial class StatusEffectsSystem : EntitySystem var query = EntityQueryEnumerator(); while (query.MoveNext(out var ent, out var effect)) { + TryApplyStatusEffect((ent, effect)); + if (effect.EndEffectTime is null) continue; @@ -88,9 +90,6 @@ public sealed partial class StatusEffectsSystem : EntitySystem statusComp.AppliedTo = ent; Dirty(args.Entity, statusComp); } - - var ev = new StatusEffectAppliedEvent(ent); - RaiseLocalEvent(args.Entity, ref ev); } private void OnEntityRemoved(Entity ent, ref EntRemovedFromContainerMessage args) @@ -121,6 +120,29 @@ public sealed partial class StatusEffectsSystem : EntitySystem PredictedQueueDel(ent.Owner); } + /// + /// Applies the status effect, i.e. starts it after it has been added. Ensures delayed start times trigger when they should. + /// + /// The status effect entity. + /// Returns true if the effect is applied. + private bool TryApplyStatusEffect(Entity statusEffectEnt) + { + if (!statusEffectEnt.Comp.Applied && + statusEffectEnt.Comp.AppliedTo != null && + _timing.CurTime >= statusEffectEnt.Comp.StartEffectTime) + { + var ev = new StatusEffectAppliedEvent(statusEffectEnt.Comp.AppliedTo.Value); + RaiseLocalEvent(statusEffectEnt, ref ev); + + statusEffectEnt.Comp.Applied = true; + + DirtyField(statusEffectEnt, statusEffectEnt.Comp, nameof(StatusEffectComponent.StartEffectTime)); + return true; + } + + return false; + } + public bool CanAddStatusEffect(EntityUid uid, EntProtoId effectProto) { if (!_proto.Resolve(effectProto, out var effectProtoData)) @@ -148,12 +170,14 @@ public sealed partial class StatusEffectsSystem : EntitySystem /// 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. + /// The delay of the effect. Leave null and the effect will be immediate. /// The EntityUid of the status effect we have just created or null if we couldn't create one. private bool TryAddStatusEffect( EntityUid target, EntProtoId effectProto, [NotNullWhen(true)] out EntityUid? statusEffect, - TimeSpan? duration = null + TimeSpan? duration = null, + TimeSpan? delay = null ) { statusEffect = null; @@ -177,7 +201,13 @@ public sealed partial class StatusEffectsSystem : EntitySystem return false; statusEffect = effect; - SetStatusEffectEndTime((effect.Value, effectComp), _timing.CurTime + duration); + + var endTime = delay == null ? _timing.CurTime + duration : _timing.CurTime + delay + duration; + SetStatusEffectEndTime((effect.Value, effectComp), endTime); + var startTime = delay == null ? TimeSpan.Zero : _timing.CurTime + delay.Value; + SetStatusEffectStartTime(effect.Value, startTime); + + TryApplyStatusEffect((effect.Value, effectComp)); return true; } @@ -204,6 +234,28 @@ public sealed partial class StatusEffectsSystem : EntitySystem SetStatusEffectEndTime(effect, newEndTime); } + private void UpdateStatusEffectDelay(Entity effect, TimeSpan? delay) + { + if (!_effectQuery.Resolve(effect, ref effect.Comp)) + return; + + // It's already started! + if (_timing.CurTime >= effect.Comp.StartEffectTime) + return; + + var newStartTime = TimeSpan.Zero; + + if (delay is not null) + { + // Don't update time to a smaller timespan... + newStartTime = _timing.CurTime + delay.Value; + if (effect.Comp.StartEffectTime < newStartTime) + return; + } + + SetStatusEffectStartTime(effect, newStartTime); + } + private void AddStatusEffectTime(Entity effect, TimeSpan delta) { if (!_effectQuery.Resolve(effect, ref effect.Comp)) @@ -233,7 +285,26 @@ public sealed partial class StatusEffectsSystem : EntitySystem var ev = new StatusEffectEndTimeUpdatedEvent(appliedTo, endTime); RaiseLocalEvent(ent, ref ev); - Dirty(ent); + DirtyField(ent, ent.Comp, nameof(StatusEffectComponent.EndEffectTime)); + } + + private void SetStatusEffectStartTime(Entity ent, TimeSpan startTime) + { + if (!_effectQuery.Resolve(ent, ref ent.Comp)) + return; + + if (ent.Comp.StartEffectTime == startTime) + return; + + ent.Comp.StartEffectTime = startTime; + + if (ent.Comp.AppliedTo is not { } appliedTo) + return; // Not much we can do! + + var ev = new StatusEffectStartTimeUpdatedEvent(appliedTo, startTime); + RaiseLocalEvent(ent, ref ev); + + DirtyField(ent, ent.Comp, nameof(StatusEffectComponent.StartEffectTime)); } } @@ -262,3 +333,11 @@ public record struct BeforeStatusEffectAddedEvent(EntProtoId Effect, bool Cancel /// The new end time of the status effect, included for convenience. [ByRefEvent] public record struct StatusEffectEndTimeUpdatedEvent(EntityUid Target, TimeSpan? EndTime); + +/// +/// Raised on an effect entity when its is updated in any way. +/// +/// The entity the effect is attached to. +/// The new start time of the status effect, included for convenience. +[ByRefEvent] +public record struct StatusEffectStartTimeUpdatedEvent(EntityUid Target, TimeSpan? StartTime); diff --git a/Resources/Locale/en-US/guidebook/chemistry/effects.ftl b/Resources/Locale/en-US/guidebook/chemistry/effects.ftl index cd7bb21af2..1ab89f89a3 100644 --- a/Resources/Locale/en-US/guidebook/chemistry/effects.ftl +++ b/Resources/Locale/en-US/guidebook/chemistry/effects.ftl @@ -118,6 +118,22 @@ reagent-effect-guidebook-status-effect = } {NATURALFIXED($time, 3)} {MANY("second", $time)} of {LOC($key)} } +reagent-effect-guidebook-status-effect-delay = + { $type -> + [add] { $chance -> + [1] Causes + *[other] cause + } {LOC($key)} for at least {NATURALFIXED($time, 3)} {MANY("second", $time)} with accumulation + *[set] { $chance -> + [1] Causes + *[other] cause + } {LOC($key)} for at least {NATURALFIXED($time, 3)} {MANY("second", $time)} without accumulation + [remove]{ $chance -> + [1] Removes + *[other] remove + } {NATURALFIXED($time, 3)} {MANY("second", $time)} of {LOC($key)} + } after a {NATURALFIXED($delay, 3)} second delay + reagent-effect-guidebook-set-solution-temperature-effect = { $chance -> [1] Sets diff --git a/Resources/Prototypes/Reagents/narcotics.yml b/Resources/Prototypes/Reagents/narcotics.yml index 7f84790561..3658a50f50 100644 --- a/Resources/Prototypes/Reagents/narcotics.yml +++ b/Resources/Prototypes/Reagents/narcotics.yml @@ -304,6 +304,7 @@ min: 8 effectProto: StatusEffectForcedSleeping time: 3 + delay: 6 type: Add - type: reagent