diff --git a/Content.Client/GameObjects/Components/Mobs/ClientOverlayEffectsComponent.cs b/Content.Client/GameObjects/Components/Mobs/ClientOverlayEffectsComponent.cs index 5b5c24d579..e1a1cb8b38 100644 --- a/Content.Client/GameObjects/Components/Mobs/ClientOverlayEffectsComponent.cs +++ b/Content.Client/GameObjects/Components/Mobs/ClientOverlayEffectsComponent.cs @@ -1,18 +1,21 @@ -using System.Collections.Generic; -using Content.Client.Graphics.Overlays; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; using Content.Shared.GameObjects.Components.Mobs; +using Content.Shared.Interfaces; using Robust.Client.GameObjects; using Robust.Client.Graphics.Overlays; using Robust.Client.Interfaces.Graphics.Overlays; -using Robust.Client.Player; using Robust.Shared.GameObjects; using Robust.Shared.Interfaces.GameObjects; -using Robust.Shared.Interfaces.Network; +using Robust.Shared.Interfaces.Reflection; using Robust.Shared.IoC; using Robust.Shared.Log; -using Robust.Shared.Players; +using Robust.Shared.Utility; +using Robust.Shared.ViewVariables; -namespace Content.Client.GameObjects +namespace Content.Client.GameObjects.Components.Mobs { /// /// A character UI component which shows the current damage state of the mob (living/dead) @@ -21,48 +24,33 @@ namespace Content.Client.GameObjects [ComponentReference(typeof(SharedOverlayEffectsComponent))] public sealed class ClientOverlayEffectsComponent : SharedOverlayEffectsComponent//, ICharacterUI { - /// /// An enum representing the current state being applied to the user /// - private ScreenEffects _currentEffect = ScreenEffects.None; + private readonly List _currentEffects = new List(); + + [ViewVariables(VVAccess.ReadOnly)] + public List ActiveOverlays + { + get => _currentEffects; + set => SetEffects(value); + } #pragma warning disable 649 // Required dependencies [Dependency] private readonly IOverlayManager _overlayManager; - [Dependency] private readonly IPlayerManager _playerManager; + [Dependency] private readonly IReflectionManager _reflectionManager; #pragma warning restore 649 - /// - /// Holds the screen effects that can be applied mapped ot their relevant overlay - /// - private Dictionary _effectsDictionary; - - /// - /// Allows calculating if we need to act due to this component being controlled by the current mob - /// - private bool CurrentlyControlled => _playerManager.LocalPlayer.ControlledEntity == Owner; - - public override void OnAdd() - { - base.OnAdd(); - - _effectsDictionary = new Dictionary() - { - { ScreenEffects.CircleMask, new CircleMaskOverlay() }, - { ScreenEffects.GradientCircleMask, new GradientCircleMask() } - }; - } - public override void HandleMessage(ComponentMessage message, IComponent component) { switch (message) { case PlayerAttachedMsg _: - SetOverlay(_currentEffect); + SetEffects(ActiveOverlays); break; case PlayerDetachedMsg _: - RemoveOverlay(); + ActiveOverlays = new List(); break; } } @@ -70,42 +58,77 @@ namespace Content.Client.GameObjects public override void HandleComponentState(ComponentState curState, ComponentState nextState) { base.HandleComponentState(curState, nextState); - if (!(curState is OverlayEffectComponentState state) || _currentEffect == state.ScreenEffect) return; - SetOverlay(state.ScreenEffect); - } - - private void SetOverlay(ScreenEffects effect) - { - RemoveOverlay(); - - _currentEffect = effect; - - ApplyOverlay(); - } - - private void RemoveOverlay() - { - if (CurrentlyControlled && _currentEffect != ScreenEffects.None) + if (!(curState is OverlayEffectComponentState state) || ActiveOverlays.Equals(state.Overlays)) { - var appliedEffect = _effectsDictionary[_currentEffect]; - _overlayManager.RemoveOverlay(appliedEffect.ID); + return; } - _currentEffect = ScreenEffects.None; + ActiveOverlays = state.Overlays; } - private void ApplyOverlay() + private void SetEffects(List newOverlays) { - if (CurrentlyControlled && _currentEffect != ScreenEffects.None) + foreach (var container in ActiveOverlays.ShallowClone()) { - var overlay = _effectsDictionary[_currentEffect]; - if (_overlayManager.HasOverlay(overlay.ID)) + if (!newOverlays.Contains(container)) { - return; + RemoveOverlay(container); } - _overlayManager.AddOverlay(overlay); - Logger.InfoS("overlay", $"Changed overlay to {overlay}"); } + + foreach (var container in newOverlays) + { + if (!ActiveOverlays.Contains(container)) + { + AddOverlay(container); + } + } + } + + private void RemoveOverlay(OverlayContainer container) + { + ActiveOverlays.Remove(container); + _overlayManager.RemoveOverlay(container.ID); + } + + private void AddOverlay(OverlayContainer container) + { + ActiveOverlays.Add(container); + if (TryCreateOverlay(container, out var overlay)) + { + _overlayManager.AddOverlay(overlay); + } + else + { + Logger.ErrorS("overlay", $"Could not add overlay {container.ID}"); + } + } + + private bool TryCreateOverlay(OverlayContainer container, out Overlay overlay) + { + var overlayTypes = _reflectionManager.GetAllChildren(); + var foundType = overlayTypes.FirstOrDefault(t => t.Name == container.ID); + + if (foundType != null) + { + overlay = Activator.CreateInstance(foundType) as Overlay; + var configurable = foundType + .GetInterfaces() + .FirstOrDefault(type => + type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IConfigurable<>) + && type.GenericTypeArguments.First() == container.GetType()); + + if (configurable != null) + { + var method = overlay?.GetType().GetMethod("Configure"); + method?.Invoke(overlay, new []{ container }); + } + + return true; + } + + overlay = default; + return false; } } } diff --git a/Content.Client/Graphics/Overlays/CircleMaskOverlay.cs b/Content.Client/Graphics/Overlays/CircleMaskOverlay.cs index 3b0a1358ce..ef6832480a 100644 --- a/Content.Client/Graphics/Overlays/CircleMaskOverlay.cs +++ b/Content.Client/Graphics/Overlays/CircleMaskOverlay.cs @@ -1,4 +1,5 @@ -using Robust.Client.Graphics.Drawing; +using Content.Shared.GameObjects.Components.Mobs; +using Robust.Client.Graphics.Drawing; using Robust.Client.Graphics.Overlays; using Robust.Client.Graphics.Shaders; using Robust.Client.Interfaces.Graphics.ClientEye; @@ -17,7 +18,7 @@ namespace Content.Client.Graphics.Overlays public override OverlaySpace Space => OverlaySpace.WorldSpace; - public CircleMaskOverlay() : base(nameof(CircleMaskOverlay)) + public CircleMaskOverlay() : base(nameof(OverlayType.CircleMaskOverlay)) { IoCManager.InjectDependencies(this); Shader = _prototypeManager.Index("CircleMask").Instance(); diff --git a/Content.Client/Graphics/Overlays/FlashOverlay.cs b/Content.Client/Graphics/Overlays/FlashOverlay.cs new file mode 100644 index 0000000000..f156225434 --- /dev/null +++ b/Content.Client/Graphics/Overlays/FlashOverlay.cs @@ -0,0 +1,73 @@ +using System.Net.Mime; +using Content.Shared.GameObjects.Components.Mobs; +using Content.Shared.Interfaces; +using Robust.Client.Graphics; +using Robust.Client.Graphics.Drawing; +using Robust.Client.Graphics.Overlays; +using Robust.Client.Graphics.Shaders; +using Robust.Client.Interfaces.Graphics; +using Robust.Client.Interfaces.Graphics.ClientEye; +using Robust.Shared.Interfaces.Timing; +using Robust.Shared.IoC; +using Robust.Shared.Maths; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using Color = Robust.Shared.Maths.Color; + +namespace Content.Client.Graphics.Overlays +{ + public class FlashOverlay : Overlay, IConfigurable + { +#pragma warning disable 649 + [Dependency] private readonly IPrototypeManager _prototypeManager; + [Dependency] private readonly IClyde _displayManager; + [Dependency] private readonly IGameTiming _gameTiming; +#pragma warning restore 649 + + public override OverlaySpace Space => OverlaySpace.ScreenSpace; + private double _startTime; + private int lastsFor = 5000; + private Texture _screenshotTexture; + + public FlashOverlay() : base(nameof(OverlayType.FlashOverlay)) + { + IoCManager.InjectDependencies(this); + Shader = _prototypeManager.Index("FlashedEffect").Instance().Duplicate(); + + _startTime = _gameTiming.CurTime.TotalMilliseconds; + _displayManager.Screenshot(ScreenshotType.BeforeUI, image => + { + var rgba32Image = image.CloneAs(Configuration.Default); + _screenshotTexture = _displayManager.LoadTextureFromImage(rgba32Image); + }); + } + + protected override void Draw(DrawingHandleBase handle) + { + var percentComplete = (float) ((_gameTiming.CurTime.TotalMilliseconds - _startTime) / lastsFor); + Shader?.SetParameter("percentComplete", percentComplete); + + var screenSpaceHandle = handle as DrawingHandleScreen; + var screenSize = UIBox2.FromDimensions((0, 0), _displayManager.ScreenSize); + + if (_screenshotTexture != null) + { + screenSpaceHandle?.DrawTextureRect(_screenshotTexture, screenSize); + } + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + _screenshotTexture = null; + } + + public void Configure(TimedOverlayContainer parameters) + { + lastsFor = parameters.Length; + } + } +} diff --git a/Content.Client/Graphics/Overlays/GradientCircleMask.cs b/Content.Client/Graphics/Overlays/GradientCircleMask.cs index 72b1a7e3c3..e95fe30d83 100644 --- a/Content.Client/Graphics/Overlays/GradientCircleMask.cs +++ b/Content.Client/Graphics/Overlays/GradientCircleMask.cs @@ -1,4 +1,5 @@ -using Robust.Client.Graphics.Drawing; +using Content.Shared.GameObjects.Components.Mobs; +using Robust.Client.Graphics.Drawing; using Robust.Client.Graphics.Overlays; using Robust.Client.Graphics.Shaders; using Robust.Client.Interfaces.Graphics.ClientEye; @@ -8,7 +9,7 @@ using Robust.Shared.Prototypes; namespace Content.Client.Graphics.Overlays { - public class GradientCircleMask : Overlay + public class GradientCircleMaskOverlay : Overlay { #pragma warning disable 649 [Dependency] private readonly IPrototypeManager _prototypeManager; @@ -16,7 +17,7 @@ namespace Content.Client.Graphics.Overlays #pragma warning restore 649 public override OverlaySpace Space => OverlaySpace.WorldSpace; - public GradientCircleMask() : base(nameof(GradientCircleMask)) + public GradientCircleMaskOverlay() : base(nameof(OverlayType.GradientCircleMaskOverlay)) { IoCManager.InjectDependencies(this); Shader = _prototypeManager.Index("GradientCircleMask").Instance(); diff --git a/Content.Server/GameObjects/Components/Mobs/DamageThresholdTemplates/HumanTemplate.cs b/Content.Server/GameObjects/Components/Mobs/DamageThresholdTemplates/HumanTemplate.cs index 18a93231da..d800514df1 100644 --- a/Content.Server/GameObjects/Components/Mobs/DamageThresholdTemplates/HumanTemplate.cs +++ b/Content.Server/GameObjects/Components/Mobs/DamageThresholdTemplates/HumanTemplate.cs @@ -69,21 +69,24 @@ namespace Content.Server.GameObjects statusEffectsComponent?.ChangeStatusEffectIcon(StatusEffect.Health, "/Textures/Mob/UI/Human/human" + modifier + ".png"); - overlayComponent?.ChangeOverlay(ScreenEffects.None); + overlayComponent?.RemoveOverlay(OverlayType.GradientCircleMaskOverlay); + overlayComponent?.RemoveOverlay(OverlayType.CircleMaskOverlay); return; case ThresholdType.Critical: statusEffectsComponent?.ChangeStatusEffectIcon( StatusEffect.Health, "/Textures/Mob/UI/Human/humancrit-0.png"); - overlayComponent?.ChangeOverlay(ScreenEffects.GradientCircleMask); + overlayComponent?.ClearOverlays(); + overlayComponent?.AddOverlay(OverlayType.GradientCircleMaskOverlay); return; case ThresholdType.Death: statusEffectsComponent?.ChangeStatusEffectIcon( StatusEffect.Health, "/Textures/Mob/UI/Human/humandead.png"); - overlayComponent?.ChangeOverlay(ScreenEffects.CircleMask); + overlayComponent?.ClearOverlays(); + overlayComponent?.AddOverlay(OverlayType.CircleMaskOverlay); return; default: diff --git a/Content.Server/GameObjects/Components/Mobs/ServerOverlayEffectsComponent.cs b/Content.Server/GameObjects/Components/Mobs/ServerOverlayEffectsComponent.cs index 8cfcb33cb8..92c0b2d5d5 100644 --- a/Content.Server/GameObjects/Components/Mobs/ServerOverlayEffectsComponent.cs +++ b/Content.Server/GameObjects/Components/Mobs/ServerOverlayEffectsComponent.cs @@ -1,5 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Linq; using Content.Shared.GameObjects.Components.Mobs; using Robust.Shared.GameObjects; +using Robust.Shared.Timers; +using Robust.Shared.ViewVariables; namespace Content.Server.GameObjects.Components.Mobs { @@ -7,20 +12,49 @@ namespace Content.Server.GameObjects.Components.Mobs [ComponentReference(typeof(SharedOverlayEffectsComponent))] public sealed class ServerOverlayEffectsComponent : SharedOverlayEffectsComponent { - private ScreenEffects _currentOverlay = ScreenEffects.None; + private readonly List _currentOverlays = new List(); + + [ViewVariables(VVAccess.ReadWrite)] + private List ActiveOverlays => _currentOverlays; public override ComponentState GetComponentState() { - return new OverlayEffectComponentState(_currentOverlay); + return new OverlayEffectComponentState(_currentOverlays); } - public void ChangeOverlay(ScreenEffects effect) + public void AddOverlay(OverlayContainer container) { - if (effect == _currentOverlay) + if (!ActiveOverlays.Contains(container)) { - return; + ActiveOverlays.Add(container); + Dirty(); } - _currentOverlay = effect; + } + + public void AddOverlay(string id) => AddOverlay(new OverlayContainer(id)); + public void AddOverlay(OverlayType type) => AddOverlay(new OverlayContainer(type)); + + public void RemoveOverlay(OverlayContainer container) + { + if (ActiveOverlays.RemoveAll(c => c.Equals(container)) > 0) + { + Dirty(); + } + } + + public void RemoveOverlay(string id) + { + if (ActiveOverlays.RemoveAll(container => container.ID == id) > 0) + { + Dirty(); + } + } + + public void RemoveOverlay(OverlayType type) => RemoveOverlay(type.ToString()); + + public void ClearOverlays() + { + ActiveOverlays.Clear(); Dirty(); } } diff --git a/Content.Server/GameObjects/Components/Mobs/SpeciesComponent.cs b/Content.Server/GameObjects/Components/Mobs/SpeciesComponent.cs index 23342f3a15..ed64d334b6 100644 --- a/Content.Server/GameObjects/Components/Mobs/SpeciesComponent.cs +++ b/Content.Server/GameObjects/Components/Mobs/SpeciesComponent.cs @@ -79,7 +79,7 @@ namespace Content.Server.GameObjects statusEffectsComponent?.RemoveStatusEffect(StatusEffect.Health); Owner.TryGetComponent(out ServerOverlayEffectsComponent overlayEffectsComponent); - overlayEffectsComponent?.ChangeOverlay(ScreenEffects.None); + overlayEffectsComponent?.ClearOverlays(); } bool IActionBlocker.CanMove() diff --git a/Content.Server/GameObjects/Components/Weapon/Melee/FlashComponent.cs b/Content.Server/GameObjects/Components/Weapon/Melee/FlashComponent.cs new file mode 100644 index 0000000000..354974a685 --- /dev/null +++ b/Content.Server/GameObjects/Components/Weapon/Melee/FlashComponent.cs @@ -0,0 +1,182 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Content.Server.GameObjects.Components.Mobs; +using Content.Server.GameObjects.EntitySystems; +using Content.Server.Interfaces; +using Content.Shared.Chat; +using Content.Shared.GameObjects.Components.Mobs; +using Content.Shared.Interfaces; +using Robust.Server.GameObjects; +using Robust.Server.GameObjects.EntitySystems; +using Robust.Server.Interfaces.GameObjects; +using Robust.Server.Interfaces.Player; +using Robust.Shared.Audio; +using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Systems; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Interfaces.Network; +using Robust.Shared.Interfaces.Random; +using Robust.Shared.IoC; +using Robust.Shared.Localization; +using Robust.Shared.Serialization; +using Robust.Shared.Utility; +using Robust.Shared.ViewVariables; +using Timer = Robust.Shared.Timers.Timer; + +namespace Content.Server.GameObjects.Components.Weapon.Melee +{ + [RegisterComponent] + public class FlashComponent : MeleeWeaponComponent, IUse, IExamine + { +#pragma warning disable 649 + [Dependency] private readonly ILocalizationManager _localizationManager; + [Dependency] private readonly IEntityManager _entityManager; + [Dependency] private readonly ISharedNotifyManager _notifyManager; +#pragma warning restore 649 + + public override string Name => "Flash"; + + [ViewVariables(VVAccess.ReadWrite)] private int _flashDuration = 5000; + [ViewVariables(VVAccess.ReadWrite)] private float _flashFalloffExp = 8f; + [ViewVariables(VVAccess.ReadWrite)] private int _uses = 5; + [ViewVariables(VVAccess.ReadWrite)] private float _range = 3f; + [ViewVariables(VVAccess.ReadWrite)] private int _aoeFlashDuration = 5000 / 3; + [ViewVariables(VVAccess.ReadWrite)] private float _slowTo = 0.75f; + private bool _flashing; + + private int Uses + { + get => _uses; + set + { + _uses = value; + Dirty(); + } + } + + private bool HasUses => _uses > 0; + + public override void ExposeData(ObjectSerializer serializer) + { + base.ExposeData(serializer); + + serializer.DataField(ref _flashDuration, "duration", 5000); + serializer.DataField(ref _flashFalloffExp, "flashFalloffExp", 8f); + serializer.DataField(ref _uses, "uses", 5); + serializer.DataField(ref _range, "range", 3f); + serializer.DataField(ref _aoeFlashDuration, "aoeFlashDuration", _flashDuration / 3); + serializer.DataField(ref _slowTo, "slowTo", 0.75f); + } + + protected override bool OnHitEntities(IReadOnlyList entities, AttackEventArgs eventArgs) + { + if (entities.Count == 0) + { + return false; + } + + if (!Use(eventArgs.User)) + { + return false; + } + + foreach (var entity in entities) + { + Flash(entity, eventArgs.User); + } + + return true; + } + + public bool UseEntity(UseEntityEventArgs eventArgs) + { + if (!Use(eventArgs.User)) + { + return false; + } + + foreach (var entity in _entityManager.GetEntitiesInRange(Owner.Transform.GridPosition, _range)) + { + Flash(entity, eventArgs.User, _aoeFlashDuration); + } + + return true; + } + + private bool Use(IEntity user) + { + if (HasUses) + { + var sprite = Owner.GetComponent(); + if (--Uses == 0) + { + sprite.LayerSetState(0, "burnt"); + + _notifyManager.PopupMessage(Owner, user, "The flash burns out!"); + } + else if (!_flashing) + { + int animLayer = sprite.AddLayerWithState("flashing"); + _flashing = true; + + Timer.Spawn(400, () => + { + sprite.RemoveLayer(animLayer); + _flashing = false; + }); + } + + EntitySystem.Get().PlayAtCoords("/Audio/weapons/flash.ogg", Owner.Transform.GridPosition, + AudioParams.Default); + + return true; + } + + return false; + } + + private void Flash(IEntity entity, IEntity user) + { + Flash(entity, user, _flashDuration); + } + + // TODO: Check if target can be flashed (e.g. things like sunglasses would block a flash) + private void Flash(IEntity entity, IEntity user, int flashDuration) + { + if (entity.TryGetComponent(out ServerOverlayEffectsComponent overlayEffectsComponent)) + { + var container = new TimedOverlayContainer(nameof(OverlayType.FlashOverlay), flashDuration); + overlayEffectsComponent.AddOverlay(container); + container.StartTimer(() => overlayEffectsComponent.RemoveOverlay(container)); + } + + if (entity.TryGetComponent(out StunnableComponent stunnableComponent)) + { + stunnableComponent.Slowdown(flashDuration / 1000f, _slowTo, _slowTo); + } + + if (entity != user) + { + _notifyManager.PopupMessage(user, entity, $"{user.Name} blinds you with the {Owner.Name}"); + } + } + + public void Examine(FormattedMessage message, bool inDetailsRange) + { + if (!HasUses) + { + message.AddText("It's burnt out."); + return; + } + + if (inDetailsRange) + { + message.AddMarkup(_localizationManager.GetString( + $"The flash has [color=green]{Uses}[/color] uses remaining.")); + } + } + } +} diff --git a/Content.Server/GameObjects/Components/Weapon/Melee/MeleeWeaponComponent.cs b/Content.Server/GameObjects/Components/Weapon/Melee/MeleeWeaponComponent.cs index 29dbe2f98c..121d755527 100644 --- a/Content.Server/GameObjects/Components/Weapon/Melee/MeleeWeaponComponent.cs +++ b/Content.Server/GameObjects/Components/Weapon/Melee/MeleeWeaponComponent.cs @@ -81,9 +81,9 @@ namespace Content.Server.GameObjects.Components.Weapon.Melee serializer.DataField(ref _cooldownTime, "cooldownTime", 1f); } - public virtual bool OnHitEntities(IReadOnlyList entities) + protected virtual bool OnHitEntities(IReadOnlyList entities, AttackEventArgs eventArgs) { - return false; + return true; } void IAttack.Attack(AttackEventArgs eventArgs) @@ -112,7 +112,7 @@ namespace Content.Server.GameObjects.Components.Weapon.Melee } } - if(OnHitEntities(hitEntities)) return; + if(!OnHitEntities(hitEntities, eventArgs)) return; var audioSystem = EntitySystem.Get(); var emitter = hitEntities.Count == 0 ? eventArgs.User : hitEntities[0]; diff --git a/Content.Server/GameObjects/Components/Weapon/Melee/StunbatonComponent.cs b/Content.Server/GameObjects/Components/Weapon/Melee/StunbatonComponent.cs index 029610ba15..367c67fdac 100644 --- a/Content.Server/GameObjects/Components/Weapon/Melee/StunbatonComponent.cs +++ b/Content.Server/GameObjects/Components/Weapon/Melee/StunbatonComponent.cs @@ -86,7 +86,7 @@ namespace Content.Server.GameObjects.Components.Weapon.Melee serializer.DataField(ref _slowdownTime, "slowdownTime", 5f); } - public override bool OnHitEntities(IReadOnlyList entities) + protected override bool OnHitEntities(IReadOnlyList entities, AttackEventArgs eventArgs) { var cell = Cell; if (!Activated || entities.Count == 0 || cell == null) @@ -118,7 +118,7 @@ namespace Content.Server.GameObjects.Components.Weapon.Melee TurnOff(); } - return false; + return true; } private bool ToggleStatus(IEntity user) diff --git a/Content.Server/Mobs/Commands.cs b/Content.Server/Mobs/Commands.cs index 2b04692431..a31eac223c 100644 --- a/Content.Server/Mobs/Commands.cs +++ b/Content.Server/Mobs/Commands.cs @@ -1,4 +1,5 @@ using System.Text; +using Content.Server.GameObjects.Components.Mobs; using Content.Server.Mobs.Roles; using Content.Server.Players; using Content.Shared.Jobs; @@ -117,4 +118,52 @@ namespace Content.Server.Mobs } } } + + public class AddOverlayCommand : IClientCommand + { + public string Command => "addoverlay"; + public string Description => "Adds an overlay by its ID"; + public string Help => "addoverlay "; + + public void Execute(IConsoleShell shell, IPlayerSession player, string[] args) + { + if (args.Length != 1) + { + shell.SendText(player, "Expected 1 argument."); + return; + } + + if (player?.AttachedEntity != null) + { + if (player.AttachedEntity.TryGetComponent(out ServerOverlayEffectsComponent overlayEffectsComponent)) + { + overlayEffectsComponent.AddOverlay(args[0]); + } + } + } + } + + public class RemoveOverlayCommand : IClientCommand + { + public string Command => "rmoverlay"; + public string Description => "Removes an overlay by its ID"; + public string Help => "rmoverlay "; + + public void Execute(IConsoleShell shell, IPlayerSession player, string[] args) + { + if (args.Length != 1) + { + shell.SendText(player, "Expected 1 argument."); + return; + } + + if (player?.AttachedEntity != null) + { + if (player.AttachedEntity.TryGetComponent(out ServerOverlayEffectsComponent overlayEffectsComponent)) + { + overlayEffectsComponent.RemoveOverlay(args[0]); + } + } + } + } } diff --git a/Content.Shared/GameObjects/Components/Mobs/SharedOverlayEffectsComponent.cs b/Content.Shared/GameObjects/Components/Mobs/SharedOverlayEffectsComponent.cs index 07b2b839c2..fd68608336 100644 --- a/Content.Shared/GameObjects/Components/Mobs/SharedOverlayEffectsComponent.cs +++ b/Content.Shared/GameObjects/Components/Mobs/SharedOverlayEffectsComponent.cs @@ -1,6 +1,14 @@ using System; +using System.Collections.Generic; +using System.ComponentModel; +using JetBrains.Annotations; using Robust.Shared.GameObjects; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization; +using Robust.Shared.Timers; +using Robust.Shared.ViewVariables; +using YamlDotNet.RepresentationModel; +using Component = Robust.Shared.GameObjects.Component; namespace Content.Shared.GameObjects.Components.Mobs { @@ -13,21 +21,72 @@ namespace Content.Shared.GameObjects.Components.Mobs public sealed override uint? NetID => ContentNetIDs.OVERLAYEFFECTS; } - public enum ScreenEffects + [Serializable, NetSerializable] + public class OverlayContainer { - None, - CircleMask, - GradientCircleMask, + [ViewVariables(VVAccess.ReadOnly)] + public string ID { get; } + + public OverlayContainer([NotNull] string id) + { + ID = id; + } + + public OverlayContainer(OverlayType type) : this(type.ToString()) + { + + } + + public override bool Equals(object obj) + { + if (obj is OverlayContainer container) + { + return container.ID == ID; + } + + if (obj is string idString) + { + return idString == ID; + } + + return base.Equals(obj); + } + + public override int GetHashCode() + { + return (ID != null ? ID.GetHashCode() : 0); + } } [Serializable, NetSerializable] public class OverlayEffectComponentState : ComponentState { - public ScreenEffects ScreenEffect; + public List Overlays; - public OverlayEffectComponentState(ScreenEffects screenEffect) : base(ContentNetIDs.OVERLAYEFFECTS) + public OverlayEffectComponentState(List overlays) : base(ContentNetIDs.OVERLAYEFFECTS) { - ScreenEffect = screenEffect; + Overlays = overlays; } } + + [Serializable, NetSerializable] + public class TimedOverlayContainer : OverlayContainer + { + [ViewVariables(VVAccess.ReadOnly)] + public int Length { get; } + + public TimedOverlayContainer(string id, int length) : base(id) + { + Length = length; + } + + public void StartTimer(Action finished) => Timer.Spawn(Length, finished); + } + + public enum OverlayType + { + GradientCircleMaskOverlay, + CircleMaskOverlay, + FlashOverlay + } } diff --git a/Content.Shared/Interfaces/IConfigurable.cs b/Content.Shared/Interfaces/IConfigurable.cs new file mode 100644 index 0000000000..17fbe07880 --- /dev/null +++ b/Content.Shared/Interfaces/IConfigurable.cs @@ -0,0 +1,7 @@ +namespace Content.Shared.Interfaces +{ + public interface IConfigurable + { + public void Configure(T parameters); + } +} diff --git a/Resources/Audio/weapons/flash.ogg b/Resources/Audio/weapons/flash.ogg new file mode 100644 index 0000000000..eed2ed0d52 Binary files /dev/null and b/Resources/Audio/weapons/flash.ogg differ diff --git a/Resources/Groups/groups.yml b/Resources/Groups/groups.yml index d1b4d32d96..dc76b17683 100644 --- a/Resources/Groups/groups.yml +++ b/Resources/Groups/groups.yml @@ -74,6 +74,8 @@ - mindinfo - addrole - rmrole + - addoverlay + - rmoverlay - showtime - group - addai @@ -121,6 +123,8 @@ - mindinfo - addrole - rmrole + - addoverlay + - rmoverlay - srvpopupmsg - group - showtime diff --git a/Resources/Prototypes/Entities/Items/Weapons/security.yml b/Resources/Prototypes/Entities/Items/Weapons/security.yml index 43e28a9ef7..957bab088b 100644 --- a/Resources/Prototypes/Entities/Items/Weapons/security.yml +++ b/Resources/Prototypes/Entities/Items/Weapons/security.yml @@ -23,3 +23,30 @@ HeldPrefix: off - type: ItemCooldown + +- type: entity + name: flash + parent: BaseItem + id: Flash + components: + - type: Sprite + sprite: Objects/Melee/flash.rsi + state: flash + + - type: Icon + sprite: Objects/Melee/flash.rsi + state: flash + + - type: Flash + damage: 0 + cooldownTime: 1 + arc: smash + hitSound: /Audio/weapons/flash.ogg + slowTo: 0.7 + + - type: Item + Size: 2 + sprite: Objects/Melee/flash.rsi + + - type: ItemCooldown + diff --git a/Resources/Prototypes/Shaders/shaders.yml b/Resources/Prototypes/Shaders/shaders.yml index e278d35c9a..b206811bc0 100644 --- a/Resources/Prototypes/Shaders/shaders.yml +++ b/Resources/Prototypes/Shaders/shaders.yml @@ -7,3 +7,8 @@ id: GradientCircleMask kind: source path: "/Shaders/gradient_circle_mask.swsl" + +- type: shader + id: FlashedEffect + kind: source + path: "/Shaders/flashed_effect.swsl" diff --git a/Resources/Shaders/flashed_effect.swsl b/Resources/Shaders/flashed_effect.swsl new file mode 100644 index 0000000000..fdbe860cb7 --- /dev/null +++ b/Resources/Shaders/flashed_effect.swsl @@ -0,0 +1,18 @@ +uniform float percentComplete; +uniform float fadeFalloffExp = 8; + +void fragment() { + // Higher exponent -> stronger blinding effect + float remaining = -pow(percentComplete, fadeFalloffExp) + 1; + + // Two ghost textures that spin around the character + vec4 tex1 = texture(TEXTURE, vec2(UV.x + (0.02) * sin(TIME * 3), UV.y + (0.02) * cos(TIME * 3))); + vec4 tex2 = texture(TEXTURE, vec2(UV.x + (0.01) * sin(TIME * 2), UV.y + (0.01) * cos(TIME * 2))); + + vec4 textureMix = mix(tex1, tex2, 0.5); + + // Gradually mixes between the texture mix and a full-white texture, causing the "blinding" effect + vec4 mixed = mix(vec4(1, 1, 1, 1), textureMix, percentComplete); + + COLOR = vec4(mixed.rgb, remaining); +} diff --git a/Resources/Textures/Objects/Melee/flash.rsi/burnt.png b/Resources/Textures/Objects/Melee/flash.rsi/burnt.png new file mode 100644 index 0000000000..b35979f0f7 Binary files /dev/null and b/Resources/Textures/Objects/Melee/flash.rsi/burnt.png differ diff --git a/Resources/Textures/Objects/Melee/flash.rsi/flash.png b/Resources/Textures/Objects/Melee/flash.rsi/flash.png new file mode 100644 index 0000000000..a5afbcdae9 Binary files /dev/null and b/Resources/Textures/Objects/Melee/flash.rsi/flash.png differ diff --git a/Resources/Textures/Objects/Melee/flash.rsi/flashing.png b/Resources/Textures/Objects/Melee/flash.rsi/flashing.png new file mode 100644 index 0000000000..0508c02541 Binary files /dev/null and b/Resources/Textures/Objects/Melee/flash.rsi/flashing.png differ diff --git a/Resources/Textures/Objects/Melee/flash.rsi/inhand-left.png b/Resources/Textures/Objects/Melee/flash.rsi/inhand-left.png new file mode 100644 index 0000000000..950195a37d Binary files /dev/null and b/Resources/Textures/Objects/Melee/flash.rsi/inhand-left.png differ diff --git a/Resources/Textures/Objects/Melee/flash.rsi/inhand-right.png b/Resources/Textures/Objects/Melee/flash.rsi/inhand-right.png new file mode 100644 index 0000000000..7a17666d9c Binary files /dev/null and b/Resources/Textures/Objects/Melee/flash.rsi/inhand-right.png differ diff --git a/Resources/Textures/Objects/Melee/flash.rsi/meta.json b/Resources/Textures/Objects/Melee/flash.rsi/meta.json new file mode 100644 index 0000000000..c0299aa63d --- /dev/null +++ b/Resources/Textures/Objects/Melee/flash.rsi/meta.json @@ -0,0 +1,76 @@ +{ + "version": 1, + "size": { + "x": 32, + "y": 32 + }, + "license": "CC BY-SA 3.0", + "copyright": "Taken from CEV Eris", + "states": [ + { + "name": "flash", + "directions": 1, + "delays": [ + [ + 1.0 + ] + ] + }, + { + "name": "flashing", + "directions": 1, + "delays": [ + [ + 0.1, + 0.1, + 0.3 + ] + ] + }, + { + "name": "burnt", + "directions": 1, + "delays": [ + [ + 1.0 + ] + ] + }, + { + "name": "inhand-right", + "directions": 4, + "delays": [ + [ + 1.0 + ], + [ + 1.0 + ], + [ + 1.0 + ], + [ + 1.0 + ] + ] + }, + { + "name": "inhand-left", + "directions": 4, + "delays": [ + [ + 1.0 + ], + [ + 1.0 + ], + [ + 1.0 + ], + [ + 1.0 + ] + ] + } + ] +}