Add status effect support to Traits, change PainNumbness to be a status effect (#41646)

* Initial commit

* Review comments

* Jobify

* Prototype(effect)
This commit is contained in:
SlamBamActionman
2025-12-03 12:55:29 +01:00
committed by GitHub
parent 7cb210261c
commit d0a784b9e6
15 changed files with 159 additions and 45 deletions

View 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)
{

View 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);
}
}
}

View File

@@ -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);
}
}
}

View 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)

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

View 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

View 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;

View 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);
}

View File

@@ -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";
}

View File

@@ -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";
}

View 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;
}
}

View 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.

View File

@@ -23,7 +23,6 @@
- Muted
- Narcolepsy
- Pacified
- PainNumbness
- Paracusia
- PermanentBlindness
- Snoring

View 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

View File

@@ -83,8 +83,10 @@
name: trait-painnumbness-name
description: trait-painnumbness-desc
category: Disabilities
components:
- type: PainNumbness
specials:
- !type:ApplyStatusEffectSpecial
statusEffects:
- PainNumbnessTraitStatusEffect
- type: trait
id: Hemophilia