diff --git a/Content.Client/Entry/IgnoredComponents.cs b/Content.Client/Entry/IgnoredComponents.cs index 43e3feb619..cb9a5e9853 100644 --- a/Content.Client/Entry/IgnoredComponents.cs +++ b/Content.Client/Entry/IgnoredComponents.cs @@ -258,6 +258,7 @@ namespace Content.Client.Entry "ReagentTank", "UtilityAI", "MouseAccent", + "FlashImmunity", "GhostTakeoverAvailable", "GhostRoleMobSpawner", "GhostOnMove", diff --git a/Content.Client/Flash/FlashSystem.cs b/Content.Client/Flash/FlashSystem.cs new file mode 100644 index 0000000000..9ff86debd6 --- /dev/null +++ b/Content.Client/Flash/FlashSystem.cs @@ -0,0 +1,64 @@ +using Content.Shared.Flash; +using Robust.Client.Graphics; +using Robust.Client.Player; +using Robust.Shared.GameObjects; +using Robust.Shared.GameStates; +using Robust.Shared.IoC; +using Robust.Shared.Timing; + +namespace Content.Client.Flash +{ + public class FlashSystem : SharedFlashSystem + { + [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly IOverlayManager _overlayManager = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnFlashableHandleState); + } + + private void OnFlashableHandleState(EntityUid uid, FlashableComponent component, ref ComponentHandleState args) + { + if (args.Current is not FlashableComponentState state) + return; + + // Yes, this code is awful. I'm just porting it to an entity system so don't blame me. + if (_playerManager.LocalPlayer != null && _playerManager.LocalPlayer.Session.AttachedEntityUid != uid) + { + return; + } + + if (state.Time == default) + { + return; + } + + // Few things here: + // 1. If a shorter duration flash is applied then don't do anything + // 2. If the client-side time is later than when the flash should've ended don't do anything + var currentTime = _gameTiming.CurTime.TotalSeconds; + var newEndTime = state.Time.TotalSeconds + state.Duration; + var currentEndTime = component.LastFlash.TotalSeconds + component.Duration; + + if (currentEndTime > newEndTime) + { + return; + } + + if (currentTime > newEndTime) + { + return; + } + + component.LastFlash = state.Time; + component.Duration = state.Duration; + + var overlay = _overlayManager.GetOverlay(); + overlay.ReceiveFlash(component.Duration); + } + } +} diff --git a/Content.Client/Flash/FlashableComponent.cs b/Content.Client/Flash/FlashableComponent.cs index 2c16df833a..e843daff23 100644 --- a/Content.Client/Flash/FlashableComponent.cs +++ b/Content.Client/Flash/FlashableComponent.cs @@ -1,61 +1,12 @@ -using System; using Content.Shared.Flash; -using Robust.Client.Graphics; -using Robust.Client.Player; +using Robust.Shared.Analyzers; using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Timing; namespace Content.Client.Flash { - [RegisterComponent] + [ComponentReference(typeof(SharedFlashableComponent))] + [RegisterComponent, Friend(typeof(FlashSystem))] public sealed class FlashableComponent : SharedFlashableComponent { - private TimeSpan _startTime; - private double _duration; - - public override void HandleComponentState(ComponentState? curState, ComponentState? nextState) - { - if (curState == null) - { - return; - } - - var playerManager = IoCManager.Resolve(); - if (playerManager.LocalPlayer != null && playerManager.LocalPlayer.ControlledEntity != Owner) - { - return; - } - - var newState = (FlashComponentState) curState; - if (newState.Time == default) - { - return; - } - - // Few things here: - // 1. If a shorter duration flash is applied then don't do anything - // 2. If the client-side time is later than when the flash should've ended don't do anything - var currentTime = IoCManager.Resolve().CurTime.TotalSeconds; - var newEndTime = newState.Time.TotalSeconds + newState.Duration; - var currentEndTime = _startTime.TotalSeconds + _duration; - - if (currentEndTime > newEndTime) - { - return; - } - - if (currentTime > newEndTime) - { - return; - } - - _startTime = newState.Time; - _duration = newState.Duration; - - var overlayManager = IoCManager.Resolve(); - var overlay = overlayManager.GetOverlay(); - overlay.ReceiveFlash(_duration); - } } } diff --git a/Content.Server/Explosion/Components/SoundOnTriggerComponent.cs b/Content.Server/Explosion/Components/SoundOnTriggerComponent.cs index ac25afb8e9..cf30935438 100644 --- a/Content.Server/Explosion/Components/SoundOnTriggerComponent.cs +++ b/Content.Server/Explosion/Components/SoundOnTriggerComponent.cs @@ -14,7 +14,7 @@ namespace Content.Server.Explosion.Components public override string Name => "SoundOnTrigger"; [ViewVariables(VVAccess.ReadWrite)] - [DataField("sound", required: true)] - public SoundSpecifier Sound { get; set; } = default!; + [DataField("sound")] + public SoundSpecifier? Sound { get; set; } } } diff --git a/Content.Server/Explosion/TriggerSystem.cs b/Content.Server/Explosion/TriggerSystem.cs index bc598de1e2..e2c19f9e33 100644 --- a/Content.Server/Explosion/TriggerSystem.cs +++ b/Content.Server/Explosion/TriggerSystem.cs @@ -1,11 +1,13 @@ using System; using Content.Server.Explosion.Components; +using Content.Server.Flash; using Content.Server.Flash.Components; using Content.Shared.Acts; using Content.Shared.Audio; using JetBrains.Annotations; using Robust.Shared.Audio; using Robust.Shared.GameObjects; +using Robust.Shared.IoC; using Robust.Shared.Physics.Dynamics; using Robust.Shared.Player; using Robust.Shared.Timing; @@ -30,6 +32,8 @@ namespace Content.Server.Explosion [UsedImplicitly] public sealed class TriggerSystem : EntitySystem { + [Dependency] private readonly FlashSystem _flashSystem = default!; + public override void Initialize() { base.Initialize(); @@ -72,12 +76,13 @@ namespace Content.Server.Explosion #region Flash private void HandleFlashTrigger(EntityUid uid, FlashOnTriggerComponent component, TriggerEvent args) - { - if (component.Flashed) return; + { + if (component.Flashed) return; - FlashableComponent.FlashAreaHelper(component.Owner, component.Range, component.Duration); - component.Flashed = true; - } + // TODO Make flash durations sane ffs. + _flashSystem.FlashArea(uid, args.User?.Uid, component.Range, component.Duration * 1000f); + component.Flashed = true; + } #endregion private void HandleSoundTrigger(EntityUid uid, SoundOnTriggerComponent component, TriggerEvent args) diff --git a/Content.Server/Flash/Components/FlashComponent.cs b/Content.Server/Flash/Components/FlashComponent.cs index 47b93c14d8..f5171d7916 100644 --- a/Content.Server/Flash/Components/FlashComponent.cs +++ b/Content.Server/Flash/Components/FlashComponent.cs @@ -1,11 +1,12 @@ using Content.Shared.Sound; +using Robust.Shared.Analyzers; using Robust.Shared.GameObjects; using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.ViewVariables; namespace Content.Server.Flash.Components { - [RegisterComponent] + [RegisterComponent, Friend(typeof(FlashSystem))] public class FlashComponent : Component { public override string Name => "Flash"; diff --git a/Content.Server/Flash/Components/FlashImmunityComponent.cs b/Content.Server/Flash/Components/FlashImmunityComponent.cs new file mode 100644 index 0000000000..0e00b695d8 --- /dev/null +++ b/Content.Server/Flash/Components/FlashImmunityComponent.cs @@ -0,0 +1,17 @@ +using Robust.Shared.Analyzers; +using Robust.Shared.GameObjects; +using Robust.Shared.Serialization.Manager.Attributes; +using Robust.Shared.ViewVariables; + +namespace Content.Server.Flash.Components +{ + [RegisterComponent, Friend(typeof(FlashSystem))] + public class FlashImmunityComponent : Component + { + public override string Name => "FlashImmunity"; + + [ViewVariables(VVAccess.ReadWrite)] + [DataField("enabled")] + public bool Enabled { get; set; } = true; + } +} diff --git a/Content.Server/Flash/Components/FlashableComponent.cs b/Content.Server/Flash/Components/FlashableComponent.cs index f96d9c51a6..58b1f7fe5a 100644 --- a/Content.Server/Flash/Components/FlashableComponent.cs +++ b/Content.Server/Flash/Components/FlashableComponent.cs @@ -1,51 +1,12 @@ -using System; using Content.Shared.Flash; -using Content.Shared.Interaction.Helpers; -using Content.Shared.Physics; -using Content.Shared.Sound; -using Robust.Shared.Audio; +using Robust.Shared.Analyzers; using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Player; -using Robust.Shared.Players; -using Robust.Shared.Timing; namespace Content.Server.Flash.Components { - [RegisterComponent] + [ComponentReference(typeof(SharedFlashableComponent))] + [RegisterComponent, Friend(typeof(FlashSystem))] public sealed class FlashableComponent : SharedFlashableComponent { - [Dependency] private readonly IGameTiming _gameTiming = default!; - - private double _duration; - private TimeSpan _lastFlash; - - public void Flash(double duration) - { - _lastFlash = _gameTiming.CurTime; - _duration = duration; - Dirty(); - } - - public override ComponentState GetComponentState(ICommonSession player) - { - return new FlashComponentState(_duration, _lastFlash); - } - - public static void FlashAreaHelper(IEntity source, float range, float duration, SoundSpecifier? sound = null) - { - foreach (var entity in IoCManager.Resolve().GetEntitiesInRange(source.Transform.Coordinates, range)) - { - if (!entity.TryGetComponent(out FlashableComponent? flashable) || - !source.InRangeUnobstructed(entity, range, CollisionGroup.Opaque)) continue; - - flashable.Flash(duration); - } - - if (sound != null) - { - SoundSystem.Play(Filter.Pvs(source), sound.GetSound(), source.Transform.Coordinates); - } - } } } diff --git a/Content.Server/Flash/FlashSystem.cs b/Content.Server/Flash/FlashSystem.cs index 6de402a834..4e1cc7ca38 100644 --- a/Content.Server/Flash/FlashSystem.cs +++ b/Content.Server/Flash/FlashSystem.cs @@ -1,32 +1,44 @@ using Content.Server.Flash.Components; +using Content.Server.Inventory.Components; +using Content.Server.Items; using Content.Server.Stunnable.Components; using Content.Server.Weapon.Melee; using Content.Shared.Examine; +using Content.Shared.Flash; using Content.Shared.Interaction; +using Content.Shared.Interaction.Helpers; +using Content.Shared.Inventory; using Content.Shared.Notification.Managers; +using Content.Shared.Physics; +using Content.Shared.Sound; using Robust.Server.GameObjects; using Robust.Shared.Audio; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Localization; using Robust.Shared.Player; +using Robust.Shared.Timing; namespace Content.Server.Flash { - internal sealed class FlashSystem : EntitySystem + internal sealed class FlashSystem : SharedFlashSystem { + [Dependency] private readonly IEntityLookup _entityLookup = default!; + [Dependency] private readonly IGameTiming _gameTiming = default!; + public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnMeleeHit); - SubscribeLocalEvent(OnMeleeInteract); - SubscribeLocalEvent(OnUseInHand); - - SubscribeLocalEvent(OnExamined); + SubscribeLocalEvent(OnFlashMeleeHit); + SubscribeLocalEvent(OnFlashMeleeInteract); + SubscribeLocalEvent(OnFlashUseInHand); + SubscribeLocalEvent(OnFlashExamined); + SubscribeLocalEvent(OnInventoryFlashAttempt); + SubscribeLocalEvent(OnFlashImmunityFlashAttempt); } - public void OnMeleeHit(EntityUid uid, FlashComponent comp, MeleeHitEvent args) + private void OnFlashMeleeHit(EntityUid uid, FlashComponent comp, MeleeHitEvent args) { if (!UseFlash(comp, args.User)) { @@ -36,11 +48,11 @@ namespace Content.Server.Flash args.Handled = true; foreach (IEntity e in args.HitEntities) { - FlashEntity(e, args.User, comp.FlashDuration, comp.SlowTo); + Flash(e.Uid, args.User.Uid, uid, comp.FlashDuration, comp.SlowTo); } } - private void OnMeleeInteract(EntityUid uid, FlashComponent comp, MeleeInteractEvent args) + private void OnFlashMeleeInteract(EntityUid uid, FlashComponent comp, MeleeInteractEvent args) { if (!UseFlash(comp, args.User)) { @@ -50,20 +62,20 @@ namespace Content.Server.Flash if (args.Entity.HasComponent()) { args.CanInteract = true; - FlashEntity(args.Entity, args.User, comp.FlashDuration, comp.SlowTo); + Flash(args.Entity.Uid, args.User.Uid, uid, comp.FlashDuration, comp.SlowTo); } } - public void OnUseInHand(EntityUid uid, FlashComponent comp, UseInHandEvent args) + private void OnFlashUseInHand(EntityUid uid, FlashComponent comp, UseInHandEvent args) { if (!UseFlash(comp, args.User)) { return; } - foreach (var entity in IoCManager.Resolve().GetEntitiesInRange(comp.Owner.Transform.Coordinates, comp.Range)) + foreach (var entity in _entityLookup.GetEntitiesInRange(comp.Owner.Transform.Coordinates, comp.Range)) { - FlashEntity(entity, args.User, comp.AoeFlashDuration, comp.SlowTo); + Flash(entity.Uid, args.User.Uid, uid, comp.AoeFlashDuration, comp.SlowTo); } } @@ -100,33 +112,60 @@ namespace Content.Server.Flash return false; } - // TODO: Check if target can be flashed (e.g. things like sunglasses would block a flash) - // TODO: Merge with the code in FlashableComponent--raise an event on the target, that FlashableComponent or - // another comp will catch - private void FlashEntity(IEntity target, IEntity user, float flashDuration, float slowTo) + public void Flash(EntityUid target, EntityUid? user, EntityUid? used, float flashDuration, float slowTo, bool displayPopup = true) { - if (target.TryGetComponent(out var flashable)) + var attempt = new FlashAttemptEvent(target, user, used); + RaiseLocalEvent(target, attempt); + + if (attempt.Cancelled) + return; + + if (ComponentManager.TryGetComponent(target, out var flashable)) { - flashable.Flash(flashDuration / 1000d); + flashable.LastFlash = _gameTiming.CurTime; + flashable.Duration = flashDuration / 1000f; // TODO: Make this sane... + flashable.Dirty(); } - if (target.TryGetComponent(out var stunnableComponent)) + if (ComponentManager.TryGetComponent(target, out var stunnableComponent)) { stunnableComponent.Slowdown(flashDuration / 1000f, slowTo, slowTo); } - if (target != user) + if (displayPopup && user != null && target != user) { - user.PopupMessage(target, + // TODO Resolving the IEntity here bad. + if(EntityManager.TryGetEntity(user.Value, out var userEntity) + && EntityManager.TryGetEntity(target, out var targetEntity)) + + userEntity.PopupMessage(targetEntity, Loc.GetString( "flash-component-user-blinds-you", - ("user", user) + ("user", userEntity) ) ); } } - private void OnExamined(EntityUid uid, FlashComponent comp, ExaminedEvent args) + public void FlashArea(EntityUid source, EntityUid? user, float range, float duration, float slowTo = 0f, bool displayPopup = false, SoundSpecifier? sound = null) + { + var transform = ComponentManager.GetComponent(source); + + foreach (var entity in _entityLookup.GetEntitiesInRange(transform.Coordinates, range)) + { + if (!entity.HasComponent() || + !transform.InRangeUnobstructed(entity, range, CollisionGroup.Opaque)) continue; + + Flash(entity.Uid, user, source, duration, slowTo, displayPopup); + } + + if (sound != null) + { + SoundSystem.Play(Filter.Pvs(transform), sound.GetSound(), transform.Coordinates); + } + } + + private void OnFlashExamined(EntityUid uid, FlashComponent comp, ExaminedEvent args) { if (!comp.HasUses) { @@ -147,5 +186,32 @@ namespace Content.Server.Flash ); } } + + private void OnInventoryFlashAttempt(EntityUid uid, InventoryComponent component, FlashAttemptEvent args) + { + // Forward the event to the glasses, if any. + if(component.TryGetSlotItem(EquipmentSlotDefines.Slots.EYES, out ItemComponent? glasses)) + RaiseLocalEvent(glasses.Owner.Uid, args); + } + + private void OnFlashImmunityFlashAttempt(EntityUid uid, FlashImmunityComponent component, FlashAttemptEvent args) + { + if(component.Enabled) + args.Cancel(); + } + } + + public 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; + } } } diff --git a/Content.Shared/Flash/SharedFlashSystem.cs b/Content.Shared/Flash/SharedFlashSystem.cs new file mode 100644 index 0000000000..7ea82d2e04 --- /dev/null +++ b/Content.Shared/Flash/SharedFlashSystem.cs @@ -0,0 +1,24 @@ +using Robust.Shared.GameObjects; +using Robust.Shared.GameStates; + +namespace Content.Shared.Flash +{ + public abstract class SharedFlashSystem : EntitySystem + { + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnFlashableGetState); + } + + private void OnFlashableGetState(EntityUid uid, SharedFlashableComponent component, ref ComponentGetState args) + { + // Only send state to the player attached to the entity. + if (args.Player.AttachedEntityUid != uid) + return; + + args.State = new FlashableComponentState(component.Duration, component.LastFlash); + } + } +} diff --git a/Content.Shared/Flash/SharedFlashableComponent.cs b/Content.Shared/Flash/SharedFlashableComponent.cs index 78c48cbdd2..dc873d614e 100644 --- a/Content.Shared/Flash/SharedFlashableComponent.cs +++ b/Content.Shared/Flash/SharedFlashableComponent.cs @@ -1,23 +1,27 @@ using System; +using Robust.Shared.Analyzers; using Robust.Shared.GameObjects; using Robust.Shared.GameStates; using Robust.Shared.Serialization; namespace Content.Shared.Flash { - [NetworkedComponent()] - public class SharedFlashableComponent : Component + [NetworkedComponent, Friend(typeof(SharedFlashSystem))] + public abstract class SharedFlashableComponent : Component { public override string Name => "Flashable"; + + public float Duration { get; set; } + public TimeSpan LastFlash { get; set; } } [Serializable, NetSerializable] - public class FlashComponentState : ComponentState + public class FlashableComponentState : ComponentState { - public double Duration { get; } + public float Duration { get; } public TimeSpan Time { get; } - public FlashComponentState(double duration, TimeSpan time) + public FlashableComponentState(float duration, TimeSpan time) { Duration = duration; Time = time; diff --git a/Resources/Prototypes/Entities/Clothing/Eyes/glasses.yml b/Resources/Prototypes/Entities/Clothing/Eyes/glasses.yml index a0e5f0064f..79301d8532 100644 --- a/Resources/Prototypes/Entities/Clothing/Eyes/glasses.yml +++ b/Resources/Prototypes/Entities/Clothing/Eyes/glasses.yml @@ -68,7 +68,7 @@ parent: ClothingEyesBase id: ClothingEyesGlasses name: glasses - description: You want to wear glasses in a game? Imagine rping as physically disabled. + description: A pair of spectacular spectacles with prescription lenses. components: - type: Sprite sprite: Clothing/Eyes/Glasses/glasses.rsi @@ -85,6 +85,7 @@ sprite: Clothing/Eyes/Glasses/sunglasses.rsi - type: Clothing sprite: Clothing/Eyes/Glasses/sunglasses.rsi + - type: FlashImmunity - type: entity parent: ClothingEyesBase @@ -96,6 +97,7 @@ sprite: Clothing/Eyes/Glasses/secglasses.rsi - type: Clothing sprite: Clothing/Eyes/Glasses/secglasses.rsi + - type: FlashImmunity #Make a scanner category when these actually function and we get the trayson - type: entity