]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Add status effect support to Traits, change PainNumbness to be a status effect (...
authorSlamBamActionman <83650252+SlamBamActionman@users.noreply.github.com>
Wed, 3 Dec 2025 11:55:29 +0000 (12:55 +0100)
committerGitHub <noreply@github.com>
Wed, 3 Dec 2025 11:55:29 +0000 (11:55 +0000)
* Initial commit

* Review comments

* Jobify

* Prototype(effect)

15 files changed:
Content.Client/UserInterface/Systems/DamageOverlays/DamageOverlayUiController.cs
Content.Server/Cloning/CloningSystem.cs
Content.Server/Jobs/ApplyStatusEffectSpecial.cs [new file with mode: 0644]
Content.Server/Traits/TraitSystem.cs
Content.Shared/Cloning/CloningSettingsPrototype.cs
Content.Shared/Roles/JobSpecial.cs
Content.Shared/StatusEffectNew/StatusEffectSystem.API.cs
Content.Shared/StatusEffectNew/StatusEffectSystem.Relay.cs
Content.Shared/Traits/Assorted/PainNumbnessComponent.cs [deleted file]
Content.Shared/Traits/Assorted/PainNumbnessStatusEffectComponent.cs [new file with mode: 0644]
Content.Shared/Traits/Assorted/PainNumbnessSystem.cs
Content.Shared/Traits/TraitPrototype.cs
Resources/Prototypes/Entities/Mobs/Player/clone.yml
Resources/Prototypes/Entities/StatusEffects/body.yml
Resources/Prototypes/Traits/disabilities.yml

index 20db76554d66a433c7ffc5fb9be8908b867e67bd..f709df4b775e7b87fb950fbea9d3cc13909c1bde 100644 (file)
@@ -3,6 +3,7 @@ using Content.Shared.FixedPoint;
 using Content.Shared.Mobs;
 using Content.Shared.Mobs.Components;
 using Content.Shared.Mobs.Systems;
+using Content.Shared.StatusEffectNew;
 using Content.Shared.Traits.Assorted;
 using JetBrains.Annotations;
 using Robust.Client.Graphics;
@@ -20,6 +21,7 @@ public sealed class DamageOverlayUiController : UIController
     [Dependency] private readonly IPlayerManager _playerManager = default!;
 
     [UISystemDependency] private readonly MobThresholdSystem _mobThresholdSystem = default!;
+    [UISystemDependency] private readonly StatusEffectsSystem _statusEffects = default!;
     private Overlays.DamageOverlay _overlay = default!;
 
     public override void Initialize()
@@ -98,7 +100,7 @@ public sealed class DamageOverlayUiController : UIController
                 FixedPoint2 painLevel = 0;
                 _overlay.PainLevel = 0;
 
-                if (!EntityManager.HasComponent<PainNumbnessComponent>(entity))
+                if (!_statusEffects.TryEffectsWithComp<PainNumbnessStatusEffectComponent>(entity, out _))
                 {
                     foreach (var painDamageType in damageable.PainDamageGroups)
                     {
index 40f8a36dfaf45b0f37e89f795b14d968966575f4..4975a0b097c1c8a19d377ed282519673b8645c33 100644 (file)
@@ -9,6 +9,7 @@ using Content.Shared.Implants;
 using Content.Shared.Implants.Components;
 using Content.Shared.NameModifier.EntitySystems;
 using Content.Shared.StatusEffect;
+using Content.Shared.StatusEffectNew.Components;
 using Content.Shared.Storage;
 using Content.Shared.Storage.EntitySystems;
 using Content.Shared.Whitelist;
@@ -36,6 +37,7 @@ public sealed partial class CloningSystem : SharedCloningSystem
     [Dependency] private readonly SharedStorageSystem _storage = default!;
     [Dependency] private readonly SharedSubdermalImplantSystem _subdermalImplant = default!;
     [Dependency] private readonly NameModifierSystem _nameMod = default!;
+    [Dependency] private readonly Shared.StatusEffectNew.StatusEffectsSystem _statusEffects = default!; //TODO: This system has to support both the old and new status effect systems, until the old is able to be fully removed.
 
     /// <summary>
     ///     Spawns a clone of the given humanoid mob at the specified location or in nullspace.
@@ -75,6 +77,10 @@ public sealed partial class CloningSystem : SharedCloningSystem
         if (settings.CopyImplants)
             CopyImplants(original, clone.Value, settings.CopyInternalStorage, settings.Whitelist, settings.Blacklist);
 
+        // Copy permanent status effects
+        if (settings.CopyStatusEffects)
+            CopyStatusEffects(original, clone.Value);
+
         var originalName = _nameMod.GetBaseName(original);
 
         // Set the clone's name. The raised events will also adjust their PDA and ID card names.
@@ -267,4 +273,33 @@ public sealed partial class CloningSystem : SharedCloningSystem
         }
 
     }
+
+    /// <summary>
+    ///    Scans all permanent status effects applied to the original entity and transfers them to the clone.
+    /// </summary>
+    public void CopyStatusEffects(Entity<StatusEffectContainerComponent?> original, Entity<StatusEffectContainerComponent?> target)
+    {
+        if (!Resolve(original, ref original.Comp, false))
+            return;
+
+        if (original.Comp.ActiveStatusEffects is null)
+            return;
+
+        foreach (var effect in original.Comp.ActiveStatusEffects.ContainedEntities)
+        {
+            if (!TryComp<StatusEffectComponent>(effect, out var effectComp))
+                continue;
+
+            //We are not interested in temporary effects, only permanent ones.
+            if (effectComp.EndEffectTime is not null)
+                continue;
+
+            var effectProto = Prototype(effect);
+
+            if (effectProto is null)
+                continue;
+
+            _statusEffects.TrySetStatusEffectDuration(target, effectProto);
+        }
+    }
 }
diff --git a/Content.Server/Jobs/ApplyStatusEffectSpecial.cs b/Content.Server/Jobs/ApplyStatusEffectSpecial.cs
new file mode 100644 (file)
index 0000000..b06ce11
--- /dev/null
@@ -0,0 +1,27 @@
+using Content.Shared.Roles;
+using Content.Shared.StatusEffectNew;
+using JetBrains.Annotations;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.Jobs;
+
+/// <summary>
+/// Adds permanent status effects to the entity.
+/// TODO: Move this, and other JobSpecials, from Server to Shared.
+/// </summary>
+[UsedImplicitly]
+public sealed partial class ApplyStatusEffectSpecial : JobSpecial
+{
+    [DataField(required: true)]
+    public HashSet<EntProtoId> StatusEffects { get; private set; } = new();
+
+    public override void AfterEquip(EntityUid mob)
+    {
+        var entMan = IoCManager.Resolve<IEntityManager>();
+        var statusSystem = entMan.System<StatusEffectsSystem>();
+        foreach (var effect in StatusEffects)
+        {
+            statusSystem.TrySetStatusEffectDuration(mob, effect);
+        }
+    }
+}
index 9a634f8942627952adcf1f4e02e5ab9fd7270d1a..010cb334dae5f26dbaf837b2fd83efa11c1aef5b 100644 (file)
@@ -2,6 +2,7 @@ using Content.Shared.GameTicking;
 using Content.Shared.Hands.Components;
 using Content.Shared.Hands.EntitySystems;
 using Content.Shared.Roles;
+using Content.Shared.StatusEffectNew;
 using Content.Shared.Traits;
 using Content.Shared.Whitelist;
 using Robust.Shared.Prototypes;
@@ -13,6 +14,7 @@ public sealed class TraitSystem : EntitySystem
     [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
     [Dependency] private readonly SharedHandsSystem _sharedHandsSystem = default!;
     [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
+    [Dependency] private readonly StatusEffectsSystem _statusEffects = default!;
 
     public override void Initialize()
     {
@@ -45,7 +47,14 @@ public sealed class TraitSystem : EntitySystem
                 continue;
 
             // Add all components required by the prototype
-            EntityManager.AddComponents(args.Mob, traitPrototype.Components, false);
+            if (traitPrototype.Components.Count > 0)
+                EntityManager.AddComponents(args.Mob, traitPrototype.Components, false);
+
+            // Add all JobSpecials required by the prototype
+            foreach (var special in traitPrototype.Specials)
+            {
+                special.AfterEquip(args.Mob);
+            }
 
             // Add item required by the trait
             if (traitPrototype.TraitGear == null)
index b422f7188b94f2e105f9ae7e2268dc3620aaee89..0b531561ca9f07185cdc7eeec4a993669a1cf67e 100644 (file)
@@ -50,6 +50,12 @@ public sealed partial class CloningSettingsPrototype : IPrototype, IInheritingPr
     [DataField]
     public bool CopyImplants = true;
 
+    /// <summary>
+    ///     Should infinite status effects applied to an entity be copied or not?
+    /// </summary>
+    [DataField]
+    public bool CopyStatusEffects = true;
+
     /// <summary>
     ///     Whitelist for the equipment allowed to be copied.
     /// </summary>
index 468e939836d667fd761c2a589ac150d8aaeff6fe..8ebeb69a6d6ed9ff874dc5e567c2a4180a784902 100644 (file)
@@ -1,7 +1,9 @@
 namespace Content.Shared.Roles
 {
     /// <summary>
-    ///     Provides special hooks for when jobs get spawned in/equipped.
+    /// Provides special hooks for when jobs get spawned in/equipped.
+    /// TODO: This is being/should be utilized by more than jobs, and is really just a way to assign components/implants/status effects upon spawning. Rename this class and its derivatives in the future!
+    /// TODO: Move derivatives from Server to Shared, probably.
     /// </summary>
     [ImplicitDataDefinitionForInheritors]
     public abstract partial class JobSpecial
index ab6362746cb5791eeb5f348551d1d91ac5faad7a..a65d4fe063c8b506d68cc656a3c821cd0ed9e0ce 100644 (file)
@@ -353,6 +353,7 @@ public sealed partial class StatusEffectsSystem
     /// <summary>
     /// Returns all status effects that have the specified component.
     /// </summary>
+    /// <returns>Returns true if any entity with the specified component is found.</returns>
     public bool TryEffectsWithComp<T>(EntityUid? target, [NotNullWhen(true)] out HashSet<Entity<T, StatusEffectComponent>>? effects) where T : IComponent
     {
         effects = null;
index 3644bed45e527314d9fab06213f8f29983dd4b63..9b16aadff0f282ee07ee8587597a79f101f1f7f7 100644 (file)
@@ -1,3 +1,5 @@
+using Content.Shared.Damage.Events;
+using Content.Shared.Mobs.Events;
 using Content.Shared.Movement.Events;
 using Content.Shared.Movement.Systems;
 using Content.Shared.Rejuvenate;
@@ -25,6 +27,9 @@ public sealed partial class StatusEffectsSystem
         SubscribeLocalEvent<StatusEffectContainerComponent, StandUpAttemptEvent>(RefRelayStatusEffectEvent);
         SubscribeLocalEvent<StatusEffectContainerComponent, StunEndAttemptEvent>(RefRelayStatusEffectEvent);
 
+        SubscribeLocalEvent<StatusEffectContainerComponent, BeforeForceSayEvent>(RelayStatusEffectEvent);
+        SubscribeLocalEvent<StatusEffectContainerComponent, BeforeAlertSeverityCheckEvent>(RelayStatusEffectEvent);
+
         SubscribeLocalEvent<StatusEffectContainerComponent, AccentGetEvent>(RelayStatusEffectEvent);
     }
 
diff --git a/Content.Shared/Traits/Assorted/PainNumbnessComponent.cs b/Content.Shared/Traits/Assorted/PainNumbnessComponent.cs
deleted file mode 100644 (file)
index 9ae72c6..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-using Content.Shared.Dataset;
-using Robust.Shared.GameStates;
-using Robust.Shared.Prototypes;
-
-namespace Content.Shared.Traits.Assorted;
-
-[RegisterComponent, NetworkedComponent]
-public sealed partial class PainNumbnessComponent : Component
-{
-    /// <summary>
-    ///     The fluent string prefix to use when picking a random suffix
-    ///     This is only active for those who have the pain numbness component
-    /// </summary>
-    [DataField]
-    public ProtoId<LocalizedDatasetPrototype> ForceSayNumbDataset = "ForceSayNumbDataset";
-}
diff --git a/Content.Shared/Traits/Assorted/PainNumbnessStatusEffectComponent.cs b/Content.Shared/Traits/Assorted/PainNumbnessStatusEffectComponent.cs
new file mode 100644 (file)
index 0000000..c5c340f
--- /dev/null
@@ -0,0 +1,20 @@
+using Content.Shared.Dataset;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Traits.Assorted;
+
+/// <summary>
+/// Hides the damage overlay and displays the health alert for the client controlling the entity as full.
+/// Has to be applied as a status effect.
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+public sealed partial class PainNumbnessStatusEffectComponent : Component
+{
+    /// <summary>
+    /// The fluent string prefix to use when picking a random suffix upon taking damage.
+    /// This is only active for those who have the pain numbness status effect. Set to null to prevent changing.
+    /// </summary>
+    [DataField]
+    public ProtoId<LocalizedDatasetPrototype>? ForceSayNumbDataset = "ForceSayNumbDataset";
+}
index 3ded13300d9805e398bca1f8957236c8bb838f60..688354c1611228043a943ada0fa4710d03ba0f99 100644 (file)
@@ -2,6 +2,7 @@ using Content.Shared.Damage.Events;
 using Content.Shared.Mobs.Components;
 using Content.Shared.Mobs.Events;
 using Content.Shared.Mobs.Systems;
+using Content.Shared.StatusEffectNew;
 
 namespace Content.Shared.Traits.Assorted;
 
@@ -11,36 +12,37 @@ public sealed class PainNumbnessSystem : EntitySystem
 
     public override void Initialize()
     {
-        SubscribeLocalEvent<PainNumbnessComponent, ComponentInit>(OnComponentInit);
-        SubscribeLocalEvent<PainNumbnessComponent, ComponentRemove>(OnComponentRemove);
-        SubscribeLocalEvent<PainNumbnessComponent, BeforeForceSayEvent>(OnChangeForceSay);
-        SubscribeLocalEvent<PainNumbnessComponent, BeforeAlertSeverityCheckEvent>(OnAlertSeverityCheck);
+        SubscribeLocalEvent<PainNumbnessStatusEffectComponent, StatusEffectAppliedEvent>(OnEffectApplied);
+        SubscribeLocalEvent<PainNumbnessStatusEffectComponent, StatusEffectRemovedEvent>(OnEffectRemoved);
+        SubscribeLocalEvent<PainNumbnessStatusEffectComponent, StatusEffectRelayedEvent<BeforeForceSayEvent>>(OnChangeForceSay);
+        SubscribeLocalEvent<PainNumbnessStatusEffectComponent, StatusEffectRelayedEvent<BeforeAlertSeverityCheckEvent>>(OnAlertSeverityCheck);
     }
 
-    private void OnComponentRemove(EntityUid uid, PainNumbnessComponent component, ComponentRemove args)
+    private void OnEffectApplied(Entity<PainNumbnessStatusEffectComponent> ent, ref StatusEffectAppliedEvent args)
     {
-        if (!HasComp<MobThresholdsComponent>(uid))
+        if (!HasComp<MobThresholdsComponent>(args.Target))
             return;
 
-        _mobThresholdSystem.VerifyThresholds(uid);
+        _mobThresholdSystem.VerifyThresholds(args.Target);
     }
 
-    private void OnComponentInit(EntityUid uid, PainNumbnessComponent component, ComponentInit args)
+    private void OnEffectRemoved(Entity<PainNumbnessStatusEffectComponent> ent, ref StatusEffectRemovedEvent args)
     {
-        if (!HasComp<MobThresholdsComponent>(uid))
+        if (!HasComp<MobThresholdsComponent>(args.Target))
             return;
 
-        _mobThresholdSystem.VerifyThresholds(uid);
+        _mobThresholdSystem.VerifyThresholds(args.Target);
     }
 
-    private void OnChangeForceSay(Entity<PainNumbnessComponent> ent, ref BeforeForceSayEvent args)
+    private void OnChangeForceSay(Entity<PainNumbnessStatusEffectComponent> ent, ref StatusEffectRelayedEvent<BeforeForceSayEvent> args)
     {
-        args.Prefix = ent.Comp.ForceSayNumbDataset;
+        if (ent.Comp.ForceSayNumbDataset != null)
+            args.Args.Prefix = ent.Comp.ForceSayNumbDataset.Value;
     }
 
-    private void OnAlertSeverityCheck(Entity<PainNumbnessComponent> ent, ref BeforeAlertSeverityCheckEvent args)
+    private void OnAlertSeverityCheck(Entity<PainNumbnessStatusEffectComponent> ent, ref StatusEffectRelayedEvent<BeforeAlertSeverityCheckEvent> args)
     {
-        if (args.CurrentAlert == "HumanHealth")
-            args.CancelUpdate = true;
+        if (args.Args.CurrentAlert == "HumanHealth")
+            args.Args.CancelUpdate = true;
     }
 }
index c79d3cbf308e18a1bcdce6c05e64f4f31b74ed19..376c5d4ac90dc6b0a4428faab32a796a25c15ff4 100644 (file)
@@ -1,3 +1,4 @@
+using Content.Shared.Roles;
 using Content.Shared.Whitelist;
 using Robust.Shared.Prototypes;
 
@@ -39,9 +40,17 @@ public sealed partial class TraitPrototype : IPrototype
 
     /// <summary>
     /// The components that get added to the player, when they pick this trait.
+    /// NOTE: When implementing a new trait, it's preferable to add it as a status effect instead if possible.
     /// </summary>
     [DataField]
-    public ComponentRegistry Components { get; private set; } = default!;
+    [Obsolete("Use JobSpecial instead.")]
+    public ComponentRegistry Components { get; private set; } = new();
+
+    /// <summary>
+    /// Special effects applied to the player who takes this Trait.
+    /// </summary>
+    [DataField(serverOnly: true)]
+    public List<JobSpecial> Specials { get; private set; } = new();
 
     /// <summary>
     /// Gear that is given to the player, when they pick this trait.
index c8ab57df58496fb2ef5602c7f6a2105f14f079b0..e9e4f04f102f087b127fec217735b3039abe5f3a 100644 (file)
@@ -23,7 +23,6 @@
   - Muted
   - Narcolepsy
   - Pacified
-  - PainNumbness
   - Paracusia
   - PermanentBlindness
   - Snoring
index 3765ebefd45e4a0b64a62ec7ac4c26205ffce318..4c94804884d67fd2d70eccfdb4d3ecf77517359d 100644 (file)
@@ -3,16 +3,27 @@
   id: BloodstreamStatusEffectBase
   abstract: true
   components:
-    - type: StatusEffect
-      whitelist:
-        components:
-          - Bloodstream
+  - type: StatusEffect
+    whitelist:
+      components:
+      - Bloodstream
 
 - type: entity
   parent: [ BloodstreamStatusEffectBase ]
   id: StatusEffectBloodloss
   name: bloodloss
   components:
-    - type: StutteringAccent
-    - type: DrunkStatusEffect
-    - type: RejuvenateRemovedStatusEffect
+  - type: StutteringAccent
+  - type: DrunkStatusEffect
+  - type: RejuvenateRemovedStatusEffect
+
+- type: entity
+  parent: MobStatusEffectBase
+  id: PainNumbnessTraitStatusEffect
+  components:
+  - type: StatusEffect
+    whitelist:
+      components:
+      - MobState
+      - MobThresholds
+  - type: PainNumbnessStatusEffect
index 51993d3dd117a645d6c16148af62879b3b5b7d68..de102b54eeddd6675101caf3f898b9edb4f816a8 100644 (file)
   name: trait-painnumbness-name
   description: trait-painnumbness-desc
   category: Disabilities
-  components:
-  - type: PainNumbness
+  specials:
+  - !type:ApplyStatusEffectSpecial
+    statusEffects:
+    - PainNumbnessTraitStatusEffect
 
 - type: trait
   id: Hemophilia