diff --git a/Content.Client/DeltaV/Addictions/AddictionSystem.cs b/Content.Client/DeltaV/Addictions/AddictionSystem.cs new file mode 100644 index 00000000000..75ac6969a48 --- /dev/null +++ b/Content.Client/DeltaV/Addictions/AddictionSystem.cs @@ -0,0 +1,8 @@ +using Content.Shared.DeltaV.Addictions; + +namespace Content.Client.DeltaV.Addictions; + +public sealed class AddictionSystem : SharedAddictionSystem +{ + protected override void UpdateTime(EntityUid uid) {} +} diff --git a/Content.Server/DeltaV/Addictions/AddictionSystem.cs b/Content.Server/DeltaV/Addictions/AddictionSystem.cs new file mode 100644 index 00000000000..1df7eeecbee --- /dev/null +++ b/Content.Server/DeltaV/Addictions/AddictionSystem.cs @@ -0,0 +1,89 @@ +using Content.Shared.Dataset; +using Content.Shared.DeltaV.Addictions; +using Content.Shared.Popups; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; +using Robust.Shared.Timing; + +namespace Content.Server.DeltaV.Addictions; + +public sealed class AddictionSystem : SharedAddictionSystem +{ + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly IGameTiming _timing = default!; + + // Define the numbers, we're not making another DeepFryerSystem.cs + // Minimum time between popups + private const int MinEffectInterval = 10; + + // Maximum time between popups + private const int MaxEffectInterval = 41; + + // The time to add after the last metabolism cycle + private const int SuppressionDuration = 10; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnInit); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var curTime = _timing.CurTime; + var query = EntityQueryEnumerator(); + + while (query.MoveNext(out var uid, out var component)) + { + // If it's suppressed, check if it's still supposed to be + if (component.Suppressed) + { + UpdateSuppressed(component); + continue; + } + + if (curTime < component.NextEffectTime) + continue; + + DoAddictionEffect(uid); + component.NextEffectTime = curTime + TimeSpan.FromSeconds(_random.Next(MinEffectInterval, MaxEffectInterval)); + } + } + + // Make sure we don't get a popup on the first update + private void OnInit(Entity ent, ref ComponentStartup args) + { + var curTime = _timing.CurTime; + ent.Comp.NextEffectTime = curTime + TimeSpan.FromSeconds(_random.Next(MinEffectInterval, MaxEffectInterval)); + } + + private void UpdateSuppressed(AddictedComponent component) + { + component.Suppressed = (_timing.CurTime < component.SuppressionEndTime); + } + + private string GetRandomPopup() + { + return Loc.GetString(_random.Pick(_prototypeManager.Index("AddictionEffects").Values)); + } + + private void DoAddictionEffect(EntityUid uid) + { + _popup.PopupEntity(GetRandomPopup(), uid, uid); + } + + // Called each time a reagent with the Addicted effect gets metabolized + protected override void UpdateTime(EntityUid uid) + { + if (!TryComp(uid, out var component)) + return; + + component.LastMetabolismTime = _timing.CurTime; + component.SuppressionEndTime = _timing.CurTime + TimeSpan.FromSeconds(SuppressionDuration); + UpdateSuppressed(component); + } +} diff --git a/Content.Server/DeltaV/EntityEffects/Effects/Addicted.cs b/Content.Server/DeltaV/EntityEffects/Effects/Addicted.cs new file mode 100644 index 00000000000..591e51a3d62 --- /dev/null +++ b/Content.Server/DeltaV/EntityEffects/Effects/Addicted.cs @@ -0,0 +1,29 @@ +using Content.Shared.DeltaV.Addictions; +using Content.Shared.EntityEffects; +using Robust.Shared.Prototypes; + +namespace Content.Server.EntityEffects.Effects; + +public sealed partial class Addicted : EntityEffect +{ + /// + /// How long should each metabolism cycle make the effect last for. + /// + [DataField] + public float AddictionTime = 3f; + + protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) + => Loc.GetString("reagent-effect-guidebook-addicted", ("chance", Probability)); + + public override void Effect(EntityEffectBaseArgs args) + { + var addictionTime = AddictionTime; + + if (args is EntityEffectReagentArgs reagentArgs) { + addictionTime *= reagentArgs.Scale.Float(); + } + + var addictionSystem = args.EntityManager.System(); + addictionSystem.TryApplyAddiction(args.TargetEntity, addictionTime); + } +} diff --git a/Content.Shared/DeltaV/Addictions/AddictedComponent.cs b/Content.Shared/DeltaV/Addictions/AddictedComponent.cs new file mode 100644 index 00000000000..2f9169a7fb9 --- /dev/null +++ b/Content.Shared/DeltaV/Addictions/AddictedComponent.cs @@ -0,0 +1,37 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; +using Robust.Shared.Timing; + +namespace Content.Shared.DeltaV.Addictions; + +[RegisterComponent, NetworkedComponent, Access(typeof(SharedAddictionSystem))] +[AutoGenerateComponentState, AutoGenerateComponentPause] +public sealed partial class AddictedComponent : Component +{ + /// + /// Whether to suppress pop-ups. + /// + [DataField, AutoNetworkedField] + public bool Suppressed; + + /// + /// The timestamp of last StatusEffect trigger. + /// + [DataField(serverOnly: true, customTypeSerializer: typeof(TimeOffsetSerializer))] + [AutoPausedField] + public TimeSpan? LastMetabolismTime; + + /// + /// The timestamp of the next popup. + /// + [DataField(serverOnly: true, customTypeSerializer: typeof(TimeOffsetSerializer))] + [AutoPausedField] + public TimeSpan? NextEffectTime; + + /// + /// The timestamp of the when the suppression ends + /// + [DataField(serverOnly: true, customTypeSerializer: typeof(TimeOffsetSerializer))] + [AutoPausedField] + public TimeSpan? SuppressionEndTime; +} diff --git a/Content.Shared/DeltaV/Addictions/SharedAddictionSystem.cs b/Content.Shared/DeltaV/Addictions/SharedAddictionSystem.cs new file mode 100644 index 00000000000..f58534592d9 --- /dev/null +++ b/Content.Shared/DeltaV/Addictions/SharedAddictionSystem.cs @@ -0,0 +1,30 @@ +using Content.Shared.StatusEffect; +using Robust.Shared.Prototypes; + +namespace Content.Shared.DeltaV.Addictions; + +public abstract class SharedAddictionSystem : EntitySystem +{ + [Dependency] private readonly StatusEffectsSystem _statusEffects = default!; + + public ProtoId StatusEffectKey = "Addicted"; + + protected abstract void UpdateTime(EntityUid uid); + + public virtual void TryApplyAddiction(EntityUid uid, float addictionTime, StatusEffectsComponent? status = null) + { + if (!Resolve(uid, ref status, false)) + return; + + UpdateTime(uid); + + if (!_statusEffects.HasStatusEffect(uid, StatusEffectKey, status)) + { + _statusEffects.TryAddStatusEffect(uid, StatusEffectKey, TimeSpan.FromSeconds(addictionTime), true, status); + } + else + { + _statusEffects.TryAddTime(uid, StatusEffectKey, TimeSpan.FromSeconds(addictionTime), status); + } + } +} diff --git a/Resources/Locale/en-US/deltav/guidebook/chemistry/effects.ftl b/Resources/Locale/en-US/deltav/guidebook/chemistry/effects.ftl new file mode 100644 index 00000000000..886e5b0c4f0 --- /dev/null +++ b/Resources/Locale/en-US/deltav/guidebook/chemistry/effects.ftl @@ -0,0 +1,5 @@ +reagent-effect-guidebook-addicted = + { $chance -> + [1] Causes + *[other] cause + } an addiction. diff --git a/Resources/Prototypes/DeltaV/Datasets/addictions.yml b/Resources/Prototypes/DeltaV/Datasets/addictions.yml new file mode 100644 index 00000000000..7f8bc2a6730 --- /dev/null +++ b/Resources/Prototypes/DeltaV/Datasets/addictions.yml @@ -0,0 +1,5 @@ +- type: localizedDataset + id: AddictionEffects + values: + prefix: reagent-effect-medaddiction- + count: 8 diff --git a/Resources/Prototypes/DeltaV/status_effects.yml b/Resources/Prototypes/DeltaV/status_effects.yml index e69de29bb2d..beb09d7addd 100644 --- a/Resources/Prototypes/DeltaV/status_effects.yml +++ b/Resources/Prototypes/DeltaV/status_effects.yml @@ -0,0 +1,2 @@ +- type: statusEffect + id: Addicted diff --git a/Resources/Prototypes/Entities/Mobs/Species/base.yml b/Resources/Prototypes/Entities/Mobs/Species/base.yml index adfd4303d00..7265c6ff737 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/base.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/base.yml @@ -138,6 +138,7 @@ - Adrenaline - PsionicsDisabled #Nyano - Summary: PCs can have psionics disabled. - PsionicallyInsulated #Nyano - Summary: PCs can be made insulated from psionic powers. + - Addicted # DeltaV - Psych med addictions system - type: Body prototype: Human requiredLegs: 2