diff --git a/Content.Client/Flash/FlashSystem.cs b/Content.Client/Flash/FlashSystem.cs index 146d84b990..ffabab9453 100644 --- a/Content.Client/Flash/FlashSystem.cs +++ b/Content.Client/Flash/FlashSystem.cs @@ -1,6 +1,5 @@ using Content.Shared.Flash; using Content.Shared.Flash.Components; -using Content.Shared.StatusEffect; using Robust.Client.Graphics; using Robust.Client.Player; using Robust.Shared.Player; diff --git a/Content.Server/EntityEffects/EntityEffectSystem.cs b/Content.Server/EntityEffects/EntityEffectSystem.cs index 03a0c8bb2b..49ed9866d4 100644 --- a/Content.Server/EntityEffects/EntityEffectSystem.cs +++ b/Content.Server/EntityEffects/EntityEffectSystem.cs @@ -10,7 +10,6 @@ using Content.Server.Botany; using Content.Server.Chat.Systems; using Content.Server.Emp; using Content.Server.Explosion.EntitySystems; -using Content.Server.Flash; using Content.Server.Fluids.EntitySystems; using Content.Server.Ghost.Roles.Components; using Content.Server.Medical; @@ -23,13 +22,12 @@ using Content.Server.Temperature.Systems; using Content.Server.Traits.Assorted; using Content.Server.Zombies; using Content.Shared.Atmos; -using Content.Shared.Audio; using Content.Shared.Coordinates.Helpers; using Content.Shared.EntityEffects.EffectConditions; using Content.Shared.EntityEffects.Effects.PlantMetabolism; -using Content.Shared.EntityEffects.Effects.StatusEffects; using Content.Shared.EntityEffects.Effects; using Content.Shared.EntityEffects; +using Content.Shared.Flash; using Content.Shared.Maps; using Content.Shared.Mind.Components; using Content.Shared.Popups; @@ -38,7 +36,6 @@ using Content.Shared.Zombies; using Robust.Server.GameObjects; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; -using Robust.Shared.GameObjects; using Robust.Shared.Map; using Robust.Shared.Prototypes; using Robust.Shared.Random; @@ -56,7 +53,7 @@ public sealed class EntityEffectSystem : EntitySystem [Dependency] private readonly EmpSystem _emp = default!; [Dependency] private readonly ExplosionSystem _explosion = default!; [Dependency] private readonly FlammableSystem _flammable = default!; - [Dependency] private readonly FlashSystem _flash = default!; + [Dependency] private readonly SharedFlashSystem _flash = default!; [Dependency] private readonly IMapManager _mapManager = default!; [Dependency] private readonly IPrototypeManager _protoManager = default!; [Dependency] private readonly IRobustRandom _random = default!; @@ -711,7 +708,7 @@ public sealed class EntityEffectSystem : EntitySystem args.Args.TargetEntity, null, range, - args.Effect.Duration * 1000, + args.Effect.Duration, slowTo: args.Effect.SlowTo, sound: args.Effect.Sound); diff --git a/Content.Server/Explosion/EntitySystems/TriggerSystem.cs b/Content.Server/Explosion/EntitySystems/TriggerSystem.cs index 894408d275..f052eadbd5 100644 --- a/Content.Server/Explosion/EntitySystems/TriggerSystem.cs +++ b/Content.Server/Explosion/EntitySystems/TriggerSystem.cs @@ -1,7 +1,7 @@ using Content.Server.Administration.Logs; using Content.Server.Body.Systems; using Content.Server.Explosion.Components; -using Content.Server.Flash; +using Content.Shared.Flash; using Content.Server.Electrocution; using Content.Server.Pinpointer; using Content.Shared.Chemistry.EntitySystems; @@ -69,7 +69,7 @@ namespace Content.Server.Explosion.EntitySystems { [Dependency] private readonly ExplosionSystem _explosions = default!; [Dependency] private readonly FixtureSystem _fixtures = default!; - [Dependency] private readonly FlashSystem _flashSystem = default!; + [Dependency] private readonly SharedFlashSystem _flashSystem = default!; [Dependency] private readonly SharedBroadphaseSystem _broadphase = default!; [Dependency] private readonly IAdminLogManager _adminLogger = default!; [Dependency] private readonly SharedContainerSystem _container = default!; @@ -196,8 +196,7 @@ namespace Content.Server.Explosion.EntitySystems private void HandleFlashTrigger(EntityUid uid, FlashOnTriggerComponent component, TriggerEvent args) { - // TODO Make flash durations sane ffs. - _flashSystem.FlashArea(uid, args.User, component.Range, component.Duration * 1000f, probability: component.Probability); + _flashSystem.FlashArea(uid, args.User, component.Range, component.Duration, probability: component.Probability); args.Handled = true; } diff --git a/Content.Server/Flash/Components/DamagedByFlashingComponent.cs b/Content.Server/Flash/Components/DamagedByFlashingComponent.cs deleted file mode 100644 index ef33454295..0000000000 --- a/Content.Server/Flash/Components/DamagedByFlashingComponent.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Content.Shared.Damage; -using Robust.Shared.Prototypes; - -namespace Content.Server.Flash.Components; - -[RegisterComponent, Access(typeof(DamagedByFlashingSystem))] -public sealed partial class DamagedByFlashingComponent : Component -{ - /// - /// damage from flashing - /// - [DataField(required: true), ViewVariables(VVAccess.ReadWrite)] - public DamageSpecifier FlashDamage = new(); -} diff --git a/Content.Server/Flash/Components/FlashImmunityComponent.cs b/Content.Server/Flash/Components/FlashImmunityComponent.cs deleted file mode 100644 index a982a9059f..0000000000 --- a/Content.Server/Flash/Components/FlashImmunityComponent.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Content.Server.Flash.Components; - -/// -/// Makes the entity immune to being flashed. -/// When given to clothes in the "head", "eyes" or "mask" slot it protects the wearer. -/// -[RegisterComponent, Access(typeof(FlashSystem))] -public sealed partial class FlashImmunityComponent : Component -{ - [ViewVariables(VVAccess.ReadWrite)] - [DataField("enabled")] - public bool Enabled { get; set; } = true; -} diff --git a/Content.Server/Flash/FlashSystem.cs b/Content.Server/Flash/FlashSystem.cs index 30c6491f62..14549c54d9 100644 --- a/Content.Server/Flash/FlashSystem.cs +++ b/Content.Server/Flash/FlashSystem.cs @@ -1,257 +1,5 @@ -using System.Linq; -using Content.Server.Flash.Components; -using Content.Shared.Flash.Components; -using Content.Server.Light.EntitySystems; -using Content.Server.Popups; -using Content.Server.Stunnable; -using Content.Shared.Charges.Components; -using Content.Shared.Charges.Systems; -using Content.Shared.Eye.Blinding.Components; using Content.Shared.Flash; -using Content.Shared.IdentityManagement; -using Content.Shared.Interaction.Events; -using Content.Shared.Inventory; -using Content.Shared.Tag; -using Content.Shared.Traits.Assorted; -using Content.Shared.Weapons.Melee.Events; -using Content.Shared.StatusEffect; -using Content.Shared.Examine; -using Robust.Server.Audio; -using Robust.Server.GameObjects; -using Robust.Shared.Audio; -using Robust.Shared.Random; -using InventoryComponent = Content.Shared.Inventory.InventoryComponent; -using Robust.Shared.Prototypes; -namespace Content.Server.Flash -{ - internal sealed class FlashSystem : SharedFlashSystem - { - [Dependency] private readonly AppearanceSystem _appearance = default!; - [Dependency] private readonly AudioSystem _audio = default!; - [Dependency] private readonly SharedChargesSystem _sharedCharges = default!; - [Dependency] private readonly EntityLookupSystem _entityLookup = default!; - [Dependency] private readonly SharedTransformSystem _transform = default!; - [Dependency] private readonly ExamineSystemShared _examine = default!; - [Dependency] private readonly InventorySystem _inventory = default!; - [Dependency] private readonly PopupSystem _popup = default!; - [Dependency] private readonly StunSystem _stun = default!; - [Dependency] private readonly TagSystem _tag = default!; - [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly StatusEffectsSystem _statusEffectsSystem = default!; +namespace Content.Server.Flash; - private static readonly ProtoId TrashTag = "Trash"; - - public override void Initialize() - { - base.Initialize(); - SubscribeLocalEvent(OnExamine); - SubscribeLocalEvent(OnFlashMeleeHit); - // ran before toggling light for extra-bright lantern - SubscribeLocalEvent(OnFlashUseInHand, before: new[] { typeof(HandheldLightSystem) }); - SubscribeLocalEvent(OnInventoryFlashAttempt); - SubscribeLocalEvent(OnFlashImmunityFlashAttempt); - SubscribeLocalEvent(OnPermanentBlindnessFlashAttempt); - SubscribeLocalEvent(OnTemporaryBlindnessFlashAttempt); - } - - private void OnExamine(Entity ent, ref ExaminedEvent args) - - { - args.PushMarkup(Loc.GetString("flash-protection")); - } - - private void OnFlashMeleeHit(EntityUid uid, FlashComponent comp, MeleeHitEvent args) - { - if (!args.IsHit || - !args.HitEntities.Any() || - !UseFlash(uid, comp, args.User)) - { - return; - } - - args.Handled = true; - foreach (var e in args.HitEntities) - { - Flash(e, args.User, uid, comp.FlashDuration, comp.SlowTo, melee: true, stunDuration: comp.MeleeStunDuration); - } - } - - private void OnFlashUseInHand(EntityUid uid, FlashComponent comp, UseInHandEvent args) - { - if (args.Handled || !UseFlash(uid, comp, args.User)) - return; - - args.Handled = true; - FlashArea(uid, args.User, comp.Range, comp.AoeFlashDuration, comp.SlowTo, true, comp.Probability); - } - - private bool UseFlash(EntityUid uid, FlashComponent comp, EntityUid user) - { - if (comp.Flashing) - return false; - - TryComp(uid, out var charges); - if (_sharedCharges.IsEmpty((uid, charges))) - return false; - - _sharedCharges.TryUseCharge((uid, charges)); - _audio.PlayPvs(comp.Sound, uid); - comp.Flashing = true; - _appearance.SetData(uid, FlashVisuals.Flashing, true); - - if (_sharedCharges.IsEmpty((uid, charges))) - { - _appearance.SetData(uid, FlashVisuals.Burnt, true); - _tag.AddTag(uid, TrashTag); - _popup.PopupEntity(Loc.GetString("flash-component-becomes-empty"), user); - } - - uid.SpawnTimer(400, () => - { - _appearance.SetData(uid, FlashVisuals.Flashing, false); - comp.Flashing = false; - }); - - return true; - } - - public void Flash(EntityUid target, - EntityUid? user, - EntityUid? used, - float flashDuration, - float slowTo, - bool displayPopup = true, - bool melee = false, - TimeSpan? stunDuration = null) - { - var attempt = new FlashAttemptEvent(target, user, used); - RaiseLocalEvent(target, attempt, true); - - if (attempt.Cancelled) - return; - - // don't paralyze, slowdown or convert to rev if the target is immune to flashes - if (!_statusEffectsSystem.TryAddStatusEffect(target, FlashedKey, TimeSpan.FromSeconds(flashDuration / 1000f), true)) - return; - - if (stunDuration != null) - { - _stun.TryParalyze(target, stunDuration.Value, true); - } - else - { - _stun.TrySlowdown(target, TimeSpan.FromSeconds(flashDuration / 1000f), true, - slowTo, slowTo); - } - - if (displayPopup && user != null && target != user && Exists(user.Value)) - { - _popup.PopupEntity(Loc.GetString("flash-component-user-blinds-you", - ("user", Identity.Entity(user.Value, EntityManager))), target, target); - } - - if (melee) - { - var ev = new AfterFlashedEvent(target, user, used); - if (user != null) - RaiseLocalEvent(user.Value, ref ev); - if (used != null) - RaiseLocalEvent(used.Value, ref ev); - } - } - - public override void FlashArea(Entity source, EntityUid? user, float range, float duration, float slowTo = 0.8f, bool displayPopup = false, float probability = 1f, SoundSpecifier? sound = null) - { - var transform = Transform(source); - var mapPosition = _transform.GetMapCoordinates(transform); - var statusEffectsQuery = GetEntityQuery(); - var damagedByFlashingQuery = GetEntityQuery(); - - foreach (var entity in _entityLookup.GetEntitiesInRange(transform.Coordinates, range)) - { - if (!_random.Prob(probability)) - continue; - - // Is the entity affected by the flash either through status effects or by taking damage? - if (!statusEffectsQuery.HasComponent(entity) && !damagedByFlashingQuery.HasComponent(entity)) - continue; - - // Check for entites in view - // put damagedByFlashingComponent in the predicate because shadow anomalies block vision. - if (!_examine.InRangeUnOccluded(entity, mapPosition, range, predicate: (e) => damagedByFlashingQuery.HasComponent(e))) - continue; - - // They shouldn't have flash removed in between right? - Flash(entity, user, source, duration, slowTo, displayPopup); - } - - _audio.PlayPvs(sound, source, AudioParams.Default.WithVolume(1f).WithMaxDistance(3f)); - } - - private void OnInventoryFlashAttempt(EntityUid uid, InventoryComponent component, FlashAttemptEvent args) - { - foreach (var slot in new[] { "head", "eyes", "mask" }) - { - if (args.Cancelled) - break; - if (_inventory.TryGetSlotEntity(uid, slot, out var item, component)) - RaiseLocalEvent(item.Value, args, true); - } - } - - private void OnFlashImmunityFlashAttempt(EntityUid uid, FlashImmunityComponent component, FlashAttemptEvent args) - { - if (component.Enabled) - args.Cancel(); - } - - private void OnPermanentBlindnessFlashAttempt(EntityUid uid, PermanentBlindnessComponent component, FlashAttemptEvent args) - { - // check for total blindness - if (component.Blindness == 0) - args.Cancel(); - } - - private void OnTemporaryBlindnessFlashAttempt(EntityUid uid, TemporaryBlindnessComponent component, FlashAttemptEvent args) - { - args.Cancel(); - } - } - - /// - /// Called before a flash is used to check if the attempt is cancelled by blindness, items or FlashImmunityComponent. - /// Raised on the target hit by the flash, the user of the flash and the flash used. - /// - public sealed class FlashAttemptEvent : CancellableEntityEventArgs - { - public readonly EntityUid Target; - public readonly EntityUid? User; - public readonly EntityUid? Used; - - public FlashAttemptEvent(EntityUid target, EntityUid? user, EntityUid? used) - { - Target = target; - User = user; - Used = used; - } - } - /// - /// Called after a flash is used via melee on another person to check for rev conversion. - /// Raised on the target hit by the flash, the user of the flash and the flash used. - /// - [ByRefEvent] - public readonly struct AfterFlashedEvent - { - public readonly EntityUid Target; - public readonly EntityUid? User; - public readonly EntityUid? Used; - - public AfterFlashedEvent(EntityUid target, EntityUid? user, EntityUid? used) - { - Target = target; - User = user; - Used = used; - } - } -} +public sealed class FlashSystem : SharedFlashSystem; diff --git a/Content.Server/GameTicking/Rules/RevolutionaryRuleSystem.cs b/Content.Server/GameTicking/Rules/RevolutionaryRuleSystem.cs index e6485a723f..d7a548bf0f 100644 --- a/Content.Server/GameTicking/Rules/RevolutionaryRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/RevolutionaryRuleSystem.cs @@ -1,7 +1,6 @@ using Content.Server.Administration.Logs; using Content.Server.Antag; using Content.Server.EUI; -using Content.Server.Flash; using Content.Server.GameTicking.Rules.Components; using Content.Server.Mind; using Content.Server.Popups; @@ -12,6 +11,7 @@ using Content.Server.RoundEnd; using Content.Server.Shuttles.Systems; using Content.Server.Station.Systems; using Content.Shared.Database; +using Content.Shared.Flash; using Content.Shared.GameTicking.Components; using Content.Shared.Humanoid; using Content.Shared.IdentityManagement; @@ -131,6 +131,9 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem private void OnPostFlash(EntityUid uid, HeadRevolutionaryComponent comp, ref AfterFlashedEvent ev) { + if (uid != ev.User || !ev.Melee) + return; + var alwaysConvertible = HasComp(ev.Target); if (!_mind.TryGetMind(ev.Target, out var mindId, out var mind) && !alwaysConvertible) diff --git a/Content.Shared/EntityEffects/Effects/FlashReactionEffect.cs b/Content.Shared/EntityEffects/Effects/FlashReactionEffect.cs index 5cd7f06c52..c238e94010 100644 --- a/Content.Shared/EntityEffects/Effects/FlashReactionEffect.cs +++ b/Content.Shared/EntityEffects/Effects/FlashReactionEffect.cs @@ -25,11 +25,11 @@ public sealed partial class FlashReactionEffect : EventEntityEffect - /// The time entities will be flashed in seconds. + /// The time entities will be flashed. /// The default is chosen to be better than the hand flash so it is worth using it for grenades etc. /// [DataField] - public float Duration = 4f; + public TimeSpan Duration = TimeSpan.FromSeconds(4); /// /// The prototype ID used for the visual effect. diff --git a/Content.Shared/Flash/Components/ActiveFlashComponent.cs b/Content.Shared/Flash/Components/ActiveFlashComponent.cs new file mode 100644 index 0000000000..18994f103e --- /dev/null +++ b/Content.Shared/Flash/Components/ActiveFlashComponent.cs @@ -0,0 +1,24 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Shared.Flash.Components; + +/// +/// Marks an entity with the as currently flashing. +/// Only used for an Update loop for resetting the visuals. +/// +/// +/// TODO: Replace this with something like sprite flick once that exists to get rid of the update loop. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause] +[Access(typeof(SharedFlashSystem))] +public sealed partial class ActiveFlashComponent : Component +{ + /// + /// Time at which this flash will be considered no longer active. + /// At this time this component will be removed. + /// + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + [AutoNetworkedField, AutoPausedField] + public TimeSpan ActiveUntil = TimeSpan.Zero; +} diff --git a/Content.Shared/Flash/Components/DamagedByFlashingComponent.cs b/Content.Shared/Flash/Components/DamagedByFlashingComponent.cs new file mode 100644 index 0000000000..766ee303c5 --- /dev/null +++ b/Content.Shared/Flash/Components/DamagedByFlashingComponent.cs @@ -0,0 +1,18 @@ +using Content.Shared.Damage; +using Robust.Shared.GameStates; + +namespace Content.Shared.Flash.Components; + +/// +/// This entity will take damage from flashes. +/// +[RegisterComponent, NetworkedComponent] +[Access(typeof(DamagedByFlashingSystem))] +public sealed partial class DamagedByFlashingComponent : Component +{ + /// + /// How much damage it will take. + /// + [DataField(required: true)] + public DamageSpecifier FlashDamage = new(); +} diff --git a/Content.Shared/Flash/Components/FlashComponent.cs b/Content.Shared/Flash/Components/FlashComponent.cs index 29f92eb94f..d1a8b882d9 100644 --- a/Content.Shared/Flash/Components/FlashComponent.cs +++ b/Content.Shared/Flash/Components/FlashComponent.cs @@ -1,55 +1,79 @@ using Robust.Shared.Audio; using Robust.Shared.GameStates; -using Robust.Shared.Serialization; -namespace Content.Shared.Flash.Components +namespace Content.Shared.Flash.Components; + +/// +/// Allows this entity to flash someone by using it or melee attacking with it. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[Access(typeof(SharedFlashSystem))] +public sealed partial class FlashComponent : Component { - [RegisterComponent, NetworkedComponent, Access(typeof(SharedFlashSystem))] - public sealed partial class FlashComponent : Component + /// + /// Flash the area around the entity when used in hand? + /// + [DataField, AutoNetworkedField] + public bool FlashOnUse = true; + + /// + /// Flash the target when melee attacking them? + /// + [DataField, AutoNetworkedField] + public bool FlashOnMelee = true; + + /// + /// Time the Flash will be visually flashing after use. + /// For the actual interaction delay use UseDelayComponent. + /// These two times should be the same. + /// + [DataField, AutoNetworkedField] + public TimeSpan FlashingTime = TimeSpan.FromSeconds(4); + + /// + /// For how long the target will lose vision when melee attacked with the flash. + /// + [DataField, AutoNetworkedField] + public TimeSpan MeleeDuration = TimeSpan.FromSeconds(5); + + /// + /// For how long the target will lose vision when used in hand. + /// + [DataField, AutoNetworkedField] + public TimeSpan AoeFlashDuration = TimeSpan.FromSeconds(2); + + /// + /// How long a target is stunned when a melee flash is used. + /// If null, melee flashes will not stun at all. + /// + [DataField, AutoNetworkedField] + public TimeSpan? MeleeStunDuration = TimeSpan.FromSeconds(1.5); + + /// + /// Range of the flash when using it. + /// + [DataField, AutoNetworkedField] + public float Range = 7f; + + /// + /// Movement speed multiplier for slowing down the target while they are flashed. + /// + [DataField, AutoNetworkedField] + public float SlowTo = 0.5f; + + /// + /// The sound to play when flashing. + /// + + [DataField, AutoNetworkedField] + public SoundSpecifier Sound = new SoundPathSpecifier("/Audio/Weapons/flash.ogg") { + Params = AudioParams.Default.WithVolume(1f).WithMaxDistance(3f) + }; - [DataField("duration")] - [ViewVariables(VVAccess.ReadWrite)] - public int FlashDuration { get; set; } = 5000; - - /// - /// How long a target is stunned when a melee flash is used. - /// If null, melee flashes will not stun at all - /// - [DataField] - public TimeSpan? MeleeStunDuration = TimeSpan.FromSeconds(1.5); - - [DataField("range")] - [ViewVariables(VVAccess.ReadWrite)] - public float Range { get; set; } = 7f; - - [ViewVariables(VVAccess.ReadWrite)] - [DataField("aoeFlashDuration")] - public int AoeFlashDuration { get; set; } = 2000; - - [DataField("slowTo")] - [ViewVariables(VVAccess.ReadWrite)] - public float SlowTo { get; set; } = 0.5f; - - [ViewVariables(VVAccess.ReadWrite)] - [DataField("sound")] - public SoundSpecifier Sound { get; set; } = new SoundPathSpecifier("/Audio/Weapons/flash.ogg") - { - Params = AudioParams.Default.WithVolume(1f).WithMaxDistance(3f) - }; - - public bool Flashing; - - [DataField] - public float Probability = 1f; - } - - [Serializable, NetSerializable] - public enum FlashVisuals : byte - { - BaseLayer, - LightLayer, - Burnt, - Flashing, - } + /// + /// The probability of sucessfully flashing someone. + /// + [DataField, AutoNetworkedField] + public float Probability = 1f; } diff --git a/Content.Shared/Flash/Components/FlashImmunityComponent.cs b/Content.Shared/Flash/Components/FlashImmunityComponent.cs new file mode 100644 index 0000000000..149c27c517 --- /dev/null +++ b/Content.Shared/Flash/Components/FlashImmunityComponent.cs @@ -0,0 +1,18 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Flash.Components; + +/// +/// Makes the entity immune to being flashed. +/// When given to clothes in the "head", "eyes" or "mask" slot it protects the wearer. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[Access(typeof(SharedFlashSystem))] +public sealed partial class FlashImmunityComponent : Component +{ + /// + /// Is this component currently enabled? + /// + [DataField, AutoNetworkedField] + public bool Enabled = true; +} diff --git a/Content.Shared/Flash/Components/FlashOnTriggerComponent.cs b/Content.Shared/Flash/Components/FlashOnTriggerComponent.cs index 7658ca0ae5..e735b3784a 100644 --- a/Content.Shared/Flash/Components/FlashOnTriggerComponent.cs +++ b/Content.Shared/Flash/Components/FlashOnTriggerComponent.cs @@ -7,7 +7,12 @@ namespace Content.Shared.Flash.Components; [RegisterComponent, NetworkedComponent] public sealed partial class FlashOnTriggerComponent : Component { - [DataField] public float Range = 1.0f; - [DataField] public float Duration = 8.0f; - [DataField] public float Probability = 1.0f; + [DataField] + public float Range = 1.0f; + + [DataField] + public TimeSpan Duration = TimeSpan.FromSeconds(8); + + [DataField] + public float Probability = 1.0f; } diff --git a/Content.Shared/Flash/Components/FlashedComponent.cs b/Content.Shared/Flash/Components/FlashedComponent.cs index 75bbb12304..e6c623b9ac 100644 --- a/Content.Shared/Flash/Components/FlashedComponent.cs +++ b/Content.Shared/Flash/Components/FlashedComponent.cs @@ -3,7 +3,7 @@ using Robust.Shared.GameStates; namespace Content.Shared.Flash.Components; /// -/// Exists for use as a status effect. Adds a shader to the client that obstructs vision. +/// Exists for use as a status effect. Adds a shader to the client that obstructs vision. /// [RegisterComponent, NetworkedComponent] -public sealed partial class FlashedComponent : Component { } +public sealed partial class FlashedComponent : Component; diff --git a/Content.Server/Flash/DamagedByFlashingSystem.cs b/Content.Shared/Flash/DamagedByFlashingSystem.cs similarity index 53% rename from Content.Server/Flash/DamagedByFlashingSystem.cs rename to Content.Shared/Flash/DamagedByFlashingSystem.cs index 5b4c19b8e5..103dfb663c 100644 --- a/Content.Server/Flash/DamagedByFlashingSystem.cs +++ b/Content.Shared/Flash/DamagedByFlashingSystem.cs @@ -1,7 +1,8 @@ -using Content.Server.Flash.Components; +using Content.Shared.Flash.Components; using Content.Shared.Damage; -namespace Content.Server.Flash; +namespace Content.Shared.Flash; + public sealed class DamagedByFlashingSystem : EntitySystem { [Dependency] private readonly DamageableSystem _damageable = default!; @@ -12,11 +13,14 @@ public sealed class DamagedByFlashingSystem : EntitySystem SubscribeLocalEvent(OnFlashAttempt); } + + // TODO: Attempt events should not be doing state changes. But using AfterFlashedEvent does not work because this entity cannot get the status effect. + // Best wait for Ed's status effect system rewrite. private void OnFlashAttempt(Entity ent, ref FlashAttemptEvent args) { _damageable.TryChangeDamage(ent, ent.Comp.FlashDamage); - //TODO: It would be more logical if different flashes had different power, - //and the damage would be inflicted depending on the strength of the flash. + // TODO: It would be more logical if different flashes had different power, + // and the damage would be inflicted depending on the strength of the flash. } } diff --git a/Content.Shared/Flash/FlashEvents.cs b/Content.Shared/Flash/FlashEvents.cs new file mode 100644 index 0000000000..1c18ca1676 --- /dev/null +++ b/Content.Shared/Flash/FlashEvents.cs @@ -0,0 +1,21 @@ +using Content.Shared.Inventory; + +namespace Content.Shared.Flash; + +/// +/// Called before a flash is used to check if the attempt is cancelled by blindness, items or FlashImmunityComponent. +/// Raised on the target hit by the flash and their inventory items. +/// +[ByRefEvent] +public record struct FlashAttemptEvent(EntityUid Target, EntityUid? User, EntityUid? Used, bool Cancelled = false) : IInventoryRelayEvent +{ + SlotFlags IInventoryRelayEvent.TargetSlots => SlotFlags.HEAD | SlotFlags.EYES | SlotFlags.MASK; +} + +/// +/// Called when a player is successfully flashed. +/// Raised on the target hit by the flash, the user of the flash and the flash used. +/// The Melee parameter is used to check for rev conversion. +/// +[ByRefEvent] +public record struct AfterFlashedEvent(EntityUid Target, EntityUid? User, EntityUid? Used, bool Melee); diff --git a/Content.Shared/Flash/FlashVisuals.cs b/Content.Shared/Flash/FlashVisuals.cs new file mode 100644 index 0000000000..f43780ff28 --- /dev/null +++ b/Content.Shared/Flash/FlashVisuals.cs @@ -0,0 +1,17 @@ +using Robust.Shared.Serialization; + +namespace Content.Shared.Flash; + +[Serializable, NetSerializable] +public enum FlashVisuals : byte +{ + Burnt, + Flashing, +} + +[Serializable, NetSerializable] +public enum FlashVisualLayers : byte +{ + BaseLayer, + LightLayer, +} diff --git a/Content.Shared/Flash/SharedFlashSystem.cs b/Content.Shared/Flash/SharedFlashSystem.cs index b778809887..50652f9408 100644 --- a/Content.Shared/Flash/SharedFlashSystem.cs +++ b/Content.Shared/Flash/SharedFlashSystem.cs @@ -1,15 +1,265 @@ +using Content.Shared.Charges.Components; +using Content.Shared.Charges.Systems; +using Content.Shared.Examine; +using Content.Shared.Eye.Blinding.Components; using Content.Shared.Flash.Components; +using Content.Shared.IdentityManagement; +using Content.Shared.Interaction.Events; +using Content.Shared.Inventory; +using Content.Shared.Light; +using Content.Shared.Popups; using Content.Shared.StatusEffect; +using Content.Shared.Stunnable; +using Content.Shared.Tag; +using Content.Shared.Timing; +using Content.Shared.Traits.Assorted; +using Content.Shared.Weapons.Melee.Events; using Robust.Shared.Audio; +using Robust.Shared.Audio.Systems; using Robust.Shared.Prototypes; +using Robust.Shared.Random; +using Robust.Shared.Timing; +using System.Linq; namespace Content.Shared.Flash; public abstract class SharedFlashSystem : EntitySystem { + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedChargesSystem _sharedCharges = default!; + [Dependency] private readonly EntityLookupSystem _entityLookup = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + [Dependency] private readonly ExamineSystemShared _examine = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly SharedStunSystem _stun = default!; + [Dependency] private readonly TagSystem _tag = default!; + [Dependency] private readonly StatusEffectsSystem _statusEffectsSystem = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly UseDelaySystem _useDelay = default!; + + private EntityQuery _statusEffectsQuery; + private EntityQuery _damagedByFlashingQuery; + private HashSet _entSet = new(); + + // The tag to add when a flash has no charges left. + private static readonly ProtoId TrashTag = "Trash"; + // The key string for the status effect. public ProtoId FlashedKey = "Flashed"; - public virtual void FlashArea(Entity source, EntityUid? user, float range, float duration, float slowTo = 0.8f, bool displayPopup = false, float probability = 1f, SoundSpecifier? sound = null) + public override void Initialize() { + base.Initialize(); + + SubscribeLocalEvent(OnFlashMeleeHit); + SubscribeLocalEvent(OnFlashUseInHand); + SubscribeLocalEvent(OnLightToggle); + SubscribeLocalEvent(OnPermanentBlindnessFlashAttempt); + SubscribeLocalEvent(OnTemporaryBlindnessFlashAttempt); + Subs.SubscribeWithRelay(OnFlashImmunityFlashAttempt, held: false); + SubscribeLocalEvent(OnExamine); + + _statusEffectsQuery = GetEntityQuery(); + _damagedByFlashingQuery = GetEntityQuery(); + } + + private void OnFlashMeleeHit(Entity ent, ref MeleeHitEvent args) + { + if (!ent.Comp.FlashOnMelee || + !args.IsHit || + !args.HitEntities.Any() || + !UseFlash(ent, args.User)) + { + return; + } + + args.Handled = true; + foreach (var target in args.HitEntities) + { + Flash(target, args.User, ent.Owner, ent.Comp.MeleeDuration, ent.Comp.SlowTo, melee: true, stunDuration: ent.Comp.MeleeStunDuration); + } + } + + private void OnFlashUseInHand(Entity ent, ref UseInHandEvent args) + { + if (!ent.Comp.FlashOnUse || args.Handled || !UseFlash(ent, args.User)) + return; + + args.Handled = true; + FlashArea(ent.Owner, args.User, ent.Comp.Range, ent.Comp.AoeFlashDuration, ent.Comp.SlowTo, true, ent.Comp.Probability); + } + + // needed for the flash lantern and interrogator lamp + // TODO: This is awful and all the different components for toggleable lights need to be unified and changed to use Itemtoggle + private void OnLightToggle(Entity ent, ref LightToggleEvent args) + { + if (!args.IsOn || !UseFlash(ent, null)) + return; + + FlashArea(ent.Owner, null, ent.Comp.Range, ent.Comp.AoeFlashDuration, ent.Comp.SlowTo, true, ent.Comp.Probability); + } + + /// + /// Use charges and set the visuals. + /// + /// False if no charges are left or the flash is currently in use. + private bool UseFlash(Entity ent, EntityUid? user) + { + if (_useDelay.IsDelayed(ent.Owner)) + return false; + + if (TryComp(ent.Owner, out var charges) + && _sharedCharges.IsEmpty((ent.Owner, charges))) + return false; + + _sharedCharges.TryUseCharge((ent.Owner, charges)); + _audio.PlayPredicted(ent.Comp.Sound, ent.Owner, user); + + var active = EnsureComp(ent.Owner); + active.ActiveUntil = _timing.CurTime + ent.Comp.FlashingTime; + Dirty(ent.Owner, active); + _appearance.SetData(ent.Owner, FlashVisuals.Flashing, true); + + if (_sharedCharges.IsEmpty((ent.Owner, charges))) + { + _appearance.SetData(ent.Owner, FlashVisuals.Burnt, true); + _tag.AddTag(ent.Owner, TrashTag); + _popup.PopupClient(Loc.GetString("flash-component-becomes-empty"), user); + } + + return true; + } + + /// + /// Cause an entity to be flashed, obstructing their vision, slowing them down and stunning them. + /// In case of a melee attack this will do a check for revolutionary conversion. + /// + /// The mob to be flashed. + /// The mob causing the flash, if any. + /// The item causing the flash, if any. + /// The time target will be affected by the flash. + /// Movement speed modifier applied to the flashed target. Between 0 and 1. + /// Whether or not to show a popup to the target player. + /// Was this flash caused by a melee attack? Used for checking for revolutionary conversion. + /// The time the target will be stunned. If null the target will be slowed down instead. + public void Flash( + EntityUid target, + EntityUid? user, + EntityUid? used, + TimeSpan flashDuration, + float slowTo, + bool displayPopup = true, + bool melee = false, + TimeSpan? stunDuration = null) + { + var attempt = new FlashAttemptEvent(target, user, used); + RaiseLocalEvent(target, ref attempt, true); + + if (attempt.Cancelled) + return; + + // don't paralyze, slowdown or convert to rev if the target is immune to flashes + if (!_statusEffectsSystem.TryAddStatusEffect(target, FlashedKey, flashDuration, true)) + return; + + if (stunDuration != null) + _stun.TryParalyze(target, stunDuration.Value, true); + else + _stun.TrySlowdown(target, flashDuration, true, slowTo, slowTo); + + if (displayPopup && user != null && target != user && Exists(user.Value)) + { + _popup.PopupEntity(Loc.GetString("flash-component-user-blinds-you", + ("user", Identity.Entity(user.Value, EntityManager))), target, target); + } + + var ev = new AfterFlashedEvent(target, user, used, melee); + RaiseLocalEvent(target, ref ev); + + if (user != null) + RaiseLocalEvent(user.Value, ref ev); + if (used != null) + RaiseLocalEvent(used.Value, ref ev); + } + + /// + /// Cause all entities in range of a source entity to be flashed. + /// + /// The source of the flash, which will be at the epicenter. + /// The mob causing the flash, if any. + /// The time target will be affected by the flash. + /// Movement speed modifier applied to the flashed target. Between 0 and 1. + /// Whether or not to show a popup to the target player. + /// Chance to be flashed. Rolled separately for each target in range. + /// Additional sound to play at the source. + public void FlashArea(EntityUid source, EntityUid? user, float range, TimeSpan flashDuration, float slowTo = 0.8f, bool displayPopup = false, float probability = 1f, SoundSpecifier? sound = null) + { + var transform = Transform(source); + var mapPosition = _transform.GetMapCoordinates(transform); + + _entSet.Clear(); + _entityLookup.GetEntitiesInRange(transform.Coordinates, range, _entSet); + foreach (var entity in _entSet) + { + // TODO: Use RandomPredicted https://github.com/space-wizards/RobustToolbox/pull/5849 + var rand = new System.Random((int)_timing.CurTick.Value + GetNetEntity(entity).Id); + if (!rand.Prob(probability)) + continue; + + // Is the entity affected by the flash either through status effects or by taking damage? + if (!_statusEffectsQuery.HasComponent(entity) && !_damagedByFlashingQuery.HasComponent(entity)) + continue; + + // Check for entites in view. + // Put DamagedByFlashingComponent in the predicate because shadow anomalies block vision. + if (!_examine.InRangeUnOccluded(entity, mapPosition, range, predicate: (e) => _damagedByFlashingQuery.HasComponent(e))) + continue; + + Flash(entity, user, source, flashDuration, slowTo, displayPopup); + } + + _audio.PlayPredicted(sound, source, user, AudioParams.Default.WithVolume(1f).WithMaxDistance(3f)); + } + + // Handle the flash visuals + // TODO: Replace this with something like sprite flick once that exists to get rid of the update loop. + public override void Update(float frameTime) + { + base.Update(frameTime); + + var curTime = _timing.CurTime; + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var active)) + { + // reset the visuals and remove the component + if (active.ActiveUntil < curTime) + { + _appearance.SetData(uid, FlashVisuals.Flashing, false); + RemCompDeferred(uid); + } + } + } + + private void OnPermanentBlindnessFlashAttempt(Entity ent, ref FlashAttemptEvent args) + { + // check for total blindness + if (ent.Comp.Blindness == 0) + args.Cancelled = true; + } + + private void OnTemporaryBlindnessFlashAttempt(Entity ent, ref FlashAttemptEvent args) + { + args.Cancelled = true; + } + + private void OnFlashImmunityFlashAttempt(Entity ent, ref FlashAttemptEvent args) + { + if (ent.Comp.Enabled) + args.Cancelled = true; + } + + private void OnExamine(Entity ent, ref ExaminedEvent args) + { + args.PushMarkup(Loc.GetString("flash-protection")); } } diff --git a/Content.Shared/Inventory/InventorySystem.Relay.cs b/Content.Shared/Inventory/InventorySystem.Relay.cs index 7973af35ab..f6a719a59b 100644 --- a/Content.Shared/Inventory/InventorySystem.Relay.cs +++ b/Content.Shared/Inventory/InventorySystem.Relay.cs @@ -10,6 +10,7 @@ using Content.Shared.Damage.Events; using Content.Shared.Electrocution; using Content.Shared.Explosion; using Content.Shared.Eye.Blinding.Systems; +using Content.Shared.Flash; using Content.Shared.Gravity; using Content.Shared.IdentityManagement.Components; using Content.Shared.Implants; @@ -66,6 +67,7 @@ public partial class InventorySystem SubscribeLocalEvent(RefRelayInventoryEvent); SubscribeLocalEvent(RefRelayInventoryEvent); SubscribeLocalEvent(RefRelayInventoryEvent); + SubscribeLocalEvent(RefRelayInventoryEvent); SubscribeLocalEvent(RefRelayInventoryEvent); SubscribeLocalEvent(RefRelayInventoryEvent); diff --git a/Content.Shared/Light/SharedHandheldLightSystem.cs b/Content.Shared/Light/SharedHandheldLightSystem.cs index e93ca0a041..0f507e1365 100644 --- a/Content.Shared/Light/SharedHandheldLightSystem.cs +++ b/Content.Shared/Light/SharedHandheldLightSystem.cs @@ -1,10 +1,10 @@ using Content.Shared.Actions; using Content.Shared.Clothing.EntitySystems; using Content.Shared.Item; +using Content.Shared.Light; using Content.Shared.Light.Components; using Content.Shared.Toggleable; using Content.Shared.Verbs; -using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.GameStates; using Robust.Shared.Utility; @@ -63,6 +63,9 @@ public abstract class SharedHandheldLightSystem : EntitySystem Dirty(uid, component); UpdateVisuals(uid, component); + + var ev = new LightToggleEvent(activated); + RaiseLocalEvent(uid, ev); } public void UpdateVisuals(EntityUid uid, HandheldLightComponent? component = null, AppearanceComponent? appearance = null) diff --git a/Resources/Prototypes/Entities/Objects/Misc/fluff_lights.yml b/Resources/Prototypes/Entities/Objects/Misc/fluff_lights.yml index e2a2000844..eaae4d410a 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/fluff_lights.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/fluff_lights.yml @@ -140,13 +140,13 @@ sprite: Objects/Misc/Lights/lampint.rsi layers: - state: lamp-int - map: [ "enum.FlashVisuals.BaseLayer" ] + map: [ "enum.FlashVisualLayers.BaseLayer" ] - state: lamp-int-on shader: unshaded visible: false map: [ "light" ] - state: flashing - map: [ "enum.FlashVisuals.LightLayer" ] + map: [ "enum.FlashVisualLayers.LightLayer" ] visible: false - type: Item sprite: Objects/Misc/Lights/lampint.rsi @@ -159,6 +159,10 @@ energy: 0.5 color: "#FFFFEE" - type: Flash + flashOnMelee: false + flashOnUse: false + - type: UseDelay + delay: 1 - type: LimitedCharges maxCharges: 3 - type: AutoRecharge @@ -176,10 +180,10 @@ - type: GenericVisualizer visuals: enum.FlashVisuals.Burnt: - enum.FlashVisuals.BaseLayer: + enum.FlashVisualLayers.BaseLayer: True: {state: burnt} enum.FlashVisuals.Flashing: - enum.FlashVisuals.LightLayer: + enum.FlashVisualLayers.LightLayer: True: {visible: true} False: {visible: false} diff --git a/Resources/Prototypes/Entities/Objects/Tools/lantern.yml b/Resources/Prototypes/Entities/Objects/Tools/lantern.yml index 3d25957851..24fdb88ed5 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/lantern.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/lantern.yml @@ -10,31 +10,33 @@ radiatingBehaviourId: radiating - type: LightBehaviour behaviours: - - !type:FadeBehaviour - id: radiating - maxDuration: 2.0 - startValue: 3.0 - endValue: 2.0 - isLooped: true - reverseWhenFinished: true - - !type:PulseBehaviour - id: blinking - interpolate: Nearest - maxDuration: 1.0 - minValue: 0.1 - maxValue: 2.0 - isLooped: true + - !type:FadeBehaviour + id: radiating + maxDuration: 2.0 + startValue: 3.0 + endValue: 2.0 + isLooped: true + reverseWhenFinished: true + - !type:PulseBehaviour + id: blinking + interpolate: Nearest + maxDuration: 1.0 + minValue: 0.1 + maxValue: 2.0 + isLooped: true - type: Sprite sprite: Objects/Tools/lantern.rsi layers: - - state: lantern - - state: lantern-on - shader: unshaded - visible: false - map: [ "light" ] + - state: lantern + - state: lantern-on + shader: unshaded + visible: false + map: [ "light" ] - type: Item sprite: Objects/Tools/lantern.rsi heldPrefix: off + - type: UseDelay + delay: 1 - type: PointLight enabled: false radius: 3 @@ -62,7 +64,7 @@ equippedPrefix: off quickEquip: false slots: - - Belt + - Belt - type: Tag tags: - Flashlight @@ -76,18 +78,20 @@ sprite: Objects/Tools/lantern.rsi layers: - state: lantern - map: [ "enum.FlashVisuals.BaseLayer" ] + map: [ "enum.FlashVisualLayers.BaseLayer" ] - state: lantern-on shader: unshaded visible: false map: [ "light" ] - state: flashing - map: [ "enum.FlashVisuals.LightLayer" ] + map: [ "enum.FlashVisualLayers.LightLayer" ] visible: false - type: PointLight radius: 5 energy: 10 - type: Flash + flashOnMelee: false + flashOnUse: false - type: LimitedCharges maxCharges: 15 - type: MeleeWeapon @@ -98,9 +102,9 @@ - type: GenericVisualizer visuals: enum.FlashVisuals.Burnt: - enum.FlashVisuals.BaseLayer: + enum.FlashVisualLayers.BaseLayer: True: {state: burnt} enum.FlashVisuals.Flashing: - enum.FlashVisuals.LightLayer: + enum.FlashVisualLayers.LightLayer: True: {visible: true} False: {visible: false} diff --git a/Resources/Prototypes/Entities/Objects/Weapons/security.yml b/Resources/Prototypes/Entities/Objects/Weapons/security.yml index ade0107670..7f69d77f93 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/security.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/security.yml @@ -139,9 +139,9 @@ sprite: Objects/Weapons/Melee/flash.rsi layers: - state: flash - map: [ "enum.FlashVisuals.BaseLayer" ] + map: [ "enum.FlashVisualLayers.BaseLayer" ] - state: flashing - map: [ "enum.FlashVisuals.LightLayer" ] + map: [ "enum.FlashVisualLayers.LightLayer" ] visible: false shader: unshaded - type: Flash @@ -157,16 +157,18 @@ size: Small sprite: Objects/Weapons/Melee/flash.rsi - type: UseDelay + delay: 4 # has to be the same as the FlashingTime datafield in FlashComponent + - type: UseDelayOnMeleeHit - type: StaticPrice price: 40 - type: Appearance - type: GenericVisualizer visuals: enum.FlashVisuals.Burnt: - enum.FlashVisuals.BaseLayer: + enum.FlashVisualLayers.BaseLayer: True: {state: burnt} enum.FlashVisuals.Flashing: - enum.FlashVisuals.LightLayer: + enum.FlashVisualLayers.LightLayer: True: {visible: true} False: {visible: false} - type: GuideHelp