Add a sleep delay to Nocturine (NewStatusEffect version) (#40231)

* Initial commit

* Minor fix

* Why is this uncommented here? Hmmm

* No wait this can't be here, oops

* Do better time

* Also guidebook

* Review changes

* Add rejuvination, fix mispredicts
This commit is contained in:
SlamBamActionman
2025-09-26 22:05:34 +02:00
committed by GitHub
parent dc3f5e3564
commit c7117f38ac
7 changed files with 179 additions and 33 deletions

View File

@@ -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<SleepingComponent, DamageChangedEvent>(OnDamageChanged);
SubscribeLocalEvent<SleepingComponent, EntityZombifiedEvent>(OnZombified);
SubscribeLocalEvent<SleepingComponent, MobStateChangedEvent>(OnMobStateChanged);
SubscribeLocalEvent<SleepingComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<SleepingComponent, ComponentInit>(OnCompInit);
SubscribeLocalEvent<SleepingComponent, SpeakAttemptEvent>(OnSpeakAttempt);
SubscribeLocalEvent<SleepingComponent, CanSeeAttemptEvent>(OnSeeAttempt);
SubscribeLocalEvent<SleepingComponent, PointAttemptEvent>(OnPointAttempt);
@@ -68,6 +69,7 @@ public sealed partial class SleepingSystem : EntitySystem
SubscribeLocalEvent<SleepingComponent, InteractHandEvent>(OnInteractHand);
SubscribeLocalEvent<SleepingComponent, StunEndAttemptEvent>(OnStunEndAttempt);
SubscribeLocalEvent<SleepingComponent, StandUpAttemptEvent>(OnStandUpAttempt);
SubscribeLocalEvent<SleepingComponent, RejuvenateEvent>(OnRejuvenate);
SubscribeLocalEvent<ForcedSleepingStatusEffectComponent, StatusEffectAppliedEvent>(OnStatusEffectApplied);
SubscribeLocalEvent<SleepingComponent, UnbuckleAttemptEvent>(OnUnbuckleAttempt);
@@ -133,7 +135,7 @@ public sealed partial class SleepingSystem : EntitySystem
RemComp<SpamEmitSoundComponent>(ent);
}
private void OnMapInit(Entity<SleepingComponent> ent, ref MapInitEvent args)
private void OnCompInit(Entity<SleepingComponent> 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<SleepingComponent> ent, ref RejuvenateEvent args)
{
TryWaking((ent.Owner, ent.Comp), true);
}
private void OnExamined(Entity<SleepingComponent> ent, ref ExaminedEvent args)
{
if (args.IsInDetailsRange)

View File

@@ -19,6 +19,12 @@ public sealed partial class ModifyStatusEffect : EntityEffect
[DataField]
public float Time = 2.0f;
/// <summary>
/// Delay before the effect starts. If another effect is added with a shorter delay, it takes precedence.
/// </summary>
[DataField]
public float Delay = 0f;
/// <remarks>
/// true - refresh status effect time (update to greater value), false - accumulate status effect time.
/// </remarks>
@@ -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;
}
}
/// <inheritdoc />
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),

View File

@@ -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.
/// </summary>
[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;
/// <summary>
/// When this effect will start. Set to Timespan.Zero to start the effect immediately.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField, AutoNetworkedField]
public TimeSpan StartEffectTime;
/// <summary>
/// When this effect will end. If Null, the effect lasts indefinitely.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField, AutoNetworkedField]
public TimeSpan? EndEffectTime;
/// <summary>
/// If true, this status effect has been applied. Used to ensure that <see cref="StatusEffectAppliedEvent"/> only fires once.
/// </summary>
[DataField, AutoNetworkedField]
public bool Applied;
/// <summary>
/// Whitelist, by which it is determined whether this status effect can be imposed on a particular entity.
/// </summary>

View File

@@ -13,13 +13,15 @@ public sealed partial class StatusEffectsSystem
/// <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="delay">The delay of the effect. If a start time already exists, the closest time takes precedence. Leave null for the effect to be instant.</param>
/// <param name="statusEffect">The EntityUid of the status effect we have just created or null if it doesn't exist.</param>
/// <returns>True if effect exists and its duration is set properly, false in case effect cannot be applied.</returns>
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;
}
///<inheritdoc cref="TryAddStatusEffectDuration(EntityUid,EntProtoId,out EntityUid?,TimeSpan)"/>
public bool TryAddStatusEffectDuration(EntityUid target, EntProtoId effectProto, TimeSpan duration)
///<inheritdoc cref="TryAddStatusEffectDuration(EntityUid,EntProtoId,out EntityUid?,TimeSpan,TimeSpan?)"/>
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);
}
/// <summary>
@@ -51,13 +54,15 @@ public sealed partial class StatusEffectsSystem
/// <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="delay">The delay of the effect. If a start time already exists, the closest time takes precedence. Leave null for the effect to be instant.</param>
/// <param name="statusEffect">The EntityUid of the status effect we have just created or null if it doesn't exist.</param>
/// <returns>True if effect exists and its duration is set properly, false in case effect cannot be applied.</returns>
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;
}
/// <inheritdoc cref="TrySetStatusEffectDuration(EntityUid,EntProtoId,out EntityUid?,TimeSpan?)"/>
public bool TrySetStatusEffectDuration(EntityUid target, EntProtoId effectProto, TimeSpan? duration = null)
/// <inheritdoc cref="TrySetStatusEffectDuration(EntityUid,EntProtoId,out EntityUid?,TimeSpan?,TimeSpan?)"/>
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);
}
/// <summary>
@@ -87,13 +97,15 @@ public sealed partial class StatusEffectsSystem
/// <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="delay">The delay of the effect. If a start time already exists, the closest time takes precedence. Leave null for the effect to be instant.</param>
/// <param name="statusEffect">The EntityUid of the status effect we have just created or null if it doesn't exist.</param>
/// <returns>True if effect exists and its duration is set properly, false in case effect cannot be applied.</returns>
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;
}
/// <inheritdoc cref="TryUpdateStatusEffectDuration(EntityUid,EntProtoId,out EntityUid?,TimeSpan?)"/>
public bool TryUpdateStatusEffectDuration(EntityUid target, EntProtoId effectProto, TimeSpan? duration = null)
/// <inheritdoc cref="TryUpdateStatusEffectDuration(EntityUid,EntProtoId,out EntityUid?,TimeSpan?,TimeSpan?)"/>
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);
}
/// <summary>
@@ -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;
}
}

View File

@@ -46,6 +46,8 @@ public sealed partial class StatusEffectsSystem : EntitySystem
var query = EntityQueryEnumerator<StatusEffectComponent>();
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<StatusEffectContainerComponent> ent, ref EntRemovedFromContainerMessage args)
@@ -121,6 +120,29 @@ public sealed partial class StatusEffectsSystem : EntitySystem
PredictedQueueDel(ent.Owner);
}
/// <summary>
/// Applies the status effect, i.e. starts it after it has been added. Ensures delayed start times trigger when they should.
/// </summary>
/// <param name="statusEffectEnt">The status effect entity.</param>
/// <returns>Returns true if the effect is applied.</returns>
private bool TryApplyStatusEffect(Entity<StatusEffectComponent> 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
/// <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="delay">The delay of the effect. Leave null and the effect will be immediate.</param>
/// <param name="statusEffect">The EntityUid of the status effect we have just created or null if we couldn't create one.</param>
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<StatusEffectComponent?> 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<StatusEffectComponent?> 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<StatusEffectComponent?> 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
/// <param name="EndTime">The new end time of the status effect, included for convenience.</param>
[ByRefEvent]
public record struct StatusEffectEndTimeUpdatedEvent(EntityUid Target, TimeSpan? EndTime);
/// <summary>
/// Raised on an effect entity when its <see cref="StatusEffectComponent.StartEffectTime"/> is updated in any way.
/// </summary>
/// <param name="Target">The entity the effect is attached to.</param>
/// <param name="StartTime">The new start time of the status effect, included for convenience.</param>
[ByRefEvent]
public record struct StatusEffectStartTimeUpdatedEvent(EntityUid Target, TimeSpan? StartTime);

View File

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

View File

@@ -304,6 +304,7 @@
min: 8
effectProto: StatusEffectForcedSleeping
time: 3
delay: 6
type: Add
- type: reagent