using System.Linq; using Content.Shared.Administration.Logs; using Content.Shared.Chemistry; using Content.Shared.Chemistry.Reaction; using Content.Shared.Database; using Content.Shared.EntityConditions; using Content.Shared.Localizations; using Content.Shared.Random.Helpers; using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Timing; namespace Content.Shared.EntityEffects; /// /// This handles entity effects. /// Specifically it handles the receiving of events for causing entity effects, and provides /// public API for other systems to take advantage of entity effects. /// public sealed partial class SharedEntityEffectsSystem : EntitySystem, IEntityEffectRaiser { [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly ISharedAdminLogManager _adminLog = default!; [Dependency] private readonly SharedEntityConditionsSystem _condition = default!; public override void Initialize() { SubscribeLocalEvent(OnReactive); } private void OnReactive(Entity entity, ref ReactionEntityEvent args) { if (args.Reagent.ReactiveEffects == null || entity.Comp.ReactiveGroups == null) return; var scale = args.ReagentQuantity.Quantity.Float(); foreach (var (key, val) in args.Reagent.ReactiveEffects) { if (!val.Methods.Contains(args.Method)) continue; if (!entity.Comp.ReactiveGroups.TryGetValue(key, out var group)) continue; if (!group.Contains(args.Method)) continue; ApplyEffects(entity, val.Effects, scale); } if (entity.Comp.Reactions == null) return; foreach (var entry in entity.Comp.Reactions) { if (!entry.Methods.Contains(args.Method)) continue; if (entry.Reagents == null || entry.Reagents.Contains(args.Reagent.ID)) ApplyEffects(entity, entry.Effects, scale); } } /// /// Applies a list of entity effects to a target entity. /// /// Entity being targeted by the effects /// Effects we're applying to the entity /// Optional scale multiplier for the effects public void ApplyEffects(EntityUid target, EntityEffect[] effects, float scale = 1f) { // do all effects, if conditions apply foreach (var effect in effects) { TryApplyEffect(target, effect, scale); } } /// /// Applies an entity effect to a target if all conditions pass. /// /// Target we're applying an effect to /// Effect we're applying /// Optional scale multiplier for the effect. Not all /// True if all conditions pass! public bool TryApplyEffect(EntityUid target, EntityEffect effect, float scale = 1f) { if (scale < effect.MinScale) return false; // TODO: Replace with proper random prediciton when it exists. if (effect.Probability <= 1f) { var seed = SharedRandomExtensions.HashCodeCombine(new() { (int)_timing.CurTick.Value, GetNetEntity(target).Id, 0 }); var rand = new System.Random(seed); if (!rand.Prob(effect.Probability)) return false; } // See if conditions apply if (!_condition.TryConditions(target, effect.Conditions)) return false; ApplyEffect(target, effect, scale); return true; } /// /// Applies an to a given target. /// This doesn't check conditions so you should only call this if you know what you're doing! /// /// Target we're applying an effect to /// Effect we're applying /// Optional scale multiplier for the effect. Not all public void ApplyEffect(EntityUid target, EntityEffect effect, float scale = 1f) { // Clamp the scale if the effect doesn't allow scaling. if (!effect.Scaling) scale = Math.Min(scale, 1f); if (effect.ShouldLog) { _adminLog.Add( LogType.EntityEffect, effect.LogImpact, $"Entity effect {effect.GetType().Name:effect}" + $" applied on entity {target:entity}" + $" at {Transform(target).Coordinates:coordinates}" + $" with a scale multiplier of {scale}" ); } effect.RaiseEvent(target, this, scale); } /// /// Raises an effect to an entity. You should not be calling this unless you know what you're doing. /// public void RaiseEffectEvent(EntityUid target, T effect, float scale) where T : EntityEffectBase { var effectEv = new EntityEffectEvent(effect, scale); RaiseLocalEvent(target, ref effectEv); } } /// /// This is a basic abstract entity effect containing all the data an entity effect needs to affect entities with effects... /// /// The Component that is required for the effect /// The Entity Effect itself public abstract partial class EntityEffectSystem : EntitySystem where T : Component where TEffect : EntityEffectBase { /// public override void Initialize() { SubscribeLocalEvent>(Effect); } protected abstract void Effect(Entity entity, ref EntityEffectEvent args); } /// /// Used to raise an EntityEffect without losing the type of effect. /// public interface IEntityEffectRaiser { void RaiseEffectEvent(EntityUid target, T effect, float scale) where T : EntityEffectBase; } /// /// Used to store an so it can be raised without losing the type of the condition. /// /// The Condition wer are raising. public abstract partial class EntityEffectBase : EntityEffect where T : EntityEffectBase { public override void RaiseEvent(EntityUid target, IEntityEffectRaiser raiser, float scale) { if (this is not T type) return; raiser.RaiseEffectEvent(target, type, scale); } } /// /// A basic instantaneous effect which can be applied to an entity via events. /// [ImplicitDataDefinitionForInheritors] public abstract partial class EntityEffect { public abstract void RaiseEvent(EntityUid target, IEntityEffectRaiser raiser, float scale); [DataField] public EntityCondition[]? Conditions; /// /// If our scale is less than this value, the effect fails. /// [DataField] public virtual float MinScale { get; private set; } /// /// If true, then it allows the scale multiplier to go above 1. /// [DataField] public virtual bool Scaling { get; private set; } // TODO: This should be an entity condition but guidebook relies on it heavily for formatting... /// /// Probability of the effect occuring. /// [DataField] public float Probability = 1.0f; /// /// The description of this entity effect that shows in guidebooks. /// public virtual string? EntityEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) => null; /// /// Whether this effect should be logged in admin logs. /// [ViewVariables] public virtual bool ShouldLog => true; /// /// If this effect is logged, how important is the log? /// [ViewVariables] public virtual LogImpact LogImpact => LogImpact.Low; } /// /// An Event carrying an entity effect. /// /// The Effect /// A strength scalar for the effect, defaults to 1 and typically only goes under for incomplete reactions. [ByRefEvent, Access(typeof(SharedEntityEffectsSystem))] public readonly record struct EntityEffectEvent(T Effect, float Scale) where T : EntityEffectBase { /// /// The Condition being raised in this event /// public readonly T Effect = Effect; /// /// The Scale modifier of this Effect. /// public readonly float Scale = Scale; }