diff --git a/Content.Client/Entry/EntryPoint.cs b/Content.Client/Entry/EntryPoint.cs index 1c2d70c8db6..56d1a04c4ef 100644 --- a/Content.Client/Entry/EntryPoint.cs +++ b/Content.Client/Entry/EntryPoint.cs @@ -35,6 +35,7 @@ using Robust.Shared.Prototypes; using Robust.Shared.Replays; using Robust.Shared.Timing; +using Content.Client._NF.Emp.Overlays; // Frontier namespace Content.Client.Entry { @@ -154,6 +155,7 @@ public override void PostInit() _overlayManager.AddOverlay(new SingularityOverlay()); _overlayManager.AddOverlay(new RadiationPulseOverlay()); + _overlayManager.AddOverlay(new EmpBlastOverlay()); // Frontier _chatManager.Initialize(); _clientPreferencesManager.Initialize(); _euiManager.Initialize(); diff --git a/Content.Client/_NF/Emp/Overlays/EmpBlastOverlay.cs b/Content.Client/_NF/Emp/Overlays/EmpBlastOverlay.cs new file mode 100644 index 00000000000..89e4e6fdefb --- /dev/null +++ b/Content.Client/_NF/Emp/Overlays/EmpBlastOverlay.cs @@ -0,0 +1,145 @@ +using System.Numerics; +using Content.Shared._NF.Emp.Components; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Shared.Enums; +using Robust.Shared.Graphics; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; + +namespace Content.Client._NF.Emp.Overlays +{ + public sealed class EmpBlastOverlay : Overlay + { + [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly IGameTiming _gameTiming = default!; + private TransformSystem? _transform; + + private const float PvsDist = 25.0f; + + public override OverlaySpace Space => OverlaySpace.WorldSpace; + public override bool RequestScreenTexture => true; + + private readonly ShaderInstance _baseShader; + private readonly Dictionary _blasts = new(); + + public EmpBlastOverlay() + { + IoCManager.InjectDependencies(this); + _baseShader = _prototypeManager.Index("Emp").Instance().Duplicate(); + } + + protected override bool BeforeDraw(in OverlayDrawArgs args) + { + EmpQuery(args.Viewport.Eye); + return _blasts.Count > 0; + } + + protected override void Draw(in OverlayDrawArgs args) + { + if (ScreenTexture == null) + return; + + var worldHandle = args.WorldHandle; + var viewport = args.Viewport; + + foreach ((var shd, var instance) in _blasts.Values) + { + if (instance.CurrentMapCoords.MapId != args.MapId) + continue; + + // To be clear, this needs to use "inside-viewport" pixels. + // In other words, specifically NOT IViewportControl.WorldToScreen (which uses outer coordinates). + var tempCoords = viewport.WorldToLocal(instance.CurrentMapCoords.Position); + tempCoords.Y = viewport.Size.Y - tempCoords.Y; + shd?.SetParameter("renderScale", viewport.RenderScale); + shd?.SetParameter("positionInput", tempCoords); + shd?.SetParameter("range", instance.Range); + var life = (_gameTiming.RealTime - instance.Start).TotalSeconds / instance.Duration; + shd?.SetParameter("life", (float)life); + + // There's probably a very good reason not to do this. + // Oh well! + shd?.SetParameter("SCREEN_TEXTURE", viewport.RenderTarget.Texture); + + worldHandle.UseShader(shd); + worldHandle.DrawRect(Box2.CenteredAround(instance.CurrentMapCoords.Position, new Vector2(instance.Range, instance.Range) * 2f), Color.White); + } + + worldHandle.UseShader(null); + } + + //Queries all blasts on the map and either adds or removes them from the list of rendered blasts based on whether they should be drawn (in range? on the same z-level/map? blast entity still exists?) + private void EmpQuery(IEye? currentEye) + { + _transform ??= _entityManager.System(); + + if (currentEye == null) + { + _blasts.Clear(); + return; + } + + var currentEyeLoc = currentEye.Position; + + var blasts = _entityManager.EntityQueryEnumerator(); + //Add all blasts that are not added yet but qualify + while (blasts.MoveNext(out var blastEntity, out var blast)) + { + if (!_blasts.ContainsKey(blastEntity) && BlastQualifies(blastEntity, currentEyeLoc, blast)) + { + _blasts.Add( + blastEntity, + ( + _baseShader.Duplicate(), + new EmpShaderInstance( + _transform.GetMapCoordinates(blastEntity), + blast.VisualRange, + blast.StartTime, + blast.VisualDuration + ) + ) + ); + } + } + + var activeShaderIds = _blasts.Keys; + foreach (var blastEntity in activeShaderIds) //Remove all blasts that are added and no longer qualify + { + if (_entityManager.EntityExists(blastEntity) && + _entityManager.TryGetComponent(blastEntity, out EmpBlastComponent? blast) && + BlastQualifies(blastEntity, currentEyeLoc, blast)) + { + var shaderInstance = _blasts[blastEntity]; + shaderInstance.instance.CurrentMapCoords = _transform.GetMapCoordinates(blastEntity); + shaderInstance.instance.Range = blast.VisualRange; + } + else + { + _blasts[blastEntity].shd.Dispose(); + _blasts.Remove(blastEntity); + } + } + + } + + private bool BlastQualifies(EntityUid blastEntity, MapCoordinates currentEyeLoc, EmpBlastComponent blast) + { + var transformComponent = _entityManager.GetComponent(blastEntity); + var transformSystem = _entityManager.System(); + return transformComponent.MapID == currentEyeLoc.MapId + && transformSystem.InRange(transformComponent.Coordinates, transformSystem.ToCoordinates(transformComponent.ParentUid, currentEyeLoc), PvsDist + blast.VisualRange); + } + + private sealed record EmpShaderInstance(MapCoordinates CurrentMapCoords, float Range, TimeSpan Start, float Duration) + { + public MapCoordinates CurrentMapCoords = CurrentMapCoords; + public float Range = Range; + public TimeSpan Start = Start; + public float Duration = Duration; + }; + } +} + diff --git a/Content.Server/Emp/EmpSystem.cs b/Content.Server/Emp/EmpSystem.cs index ff6fe45b14c..538a9521561 100644 --- a/Content.Server/Emp/EmpSystem.cs +++ b/Content.Server/Emp/EmpSystem.cs @@ -10,6 +10,10 @@ using Content.Shared.Tiles; // Frontier using Robust.Server.GameObjects; using Robust.Shared.Map; +using Content.Shared._NF.Emp.Components; // Frontier +using Robust.Server.GameStates; // Frontier: EMP Blast PVS +using Robust.Shared.Configuration; // Frontier: EMP Blast PVS +using Robust.Shared; // Frontier: EMP Blast PVS namespace Content.Server.Emp; @@ -17,8 +21,10 @@ public sealed class EmpSystem : SharedEmpSystem { [Dependency] private readonly EntityLookupSystem _lookup = default!; [Dependency] private readonly TransformSystem _transform = default!; + [Dependency] private readonly PvsOverrideSystem _pvs = default!; // Frontier: EMP Blast PVS + [Dependency] private readonly IConfigurationManager _cfg = default!; // Frontier: EMP Blast PVS - public const string EmpPulseEffectPrototype = "EffectEmpPulse"; + public const string EmpPulseEffectPrototype = "EffectEmpBlast"; // Frontier: EffectEmpPulse public override void Initialize() { @@ -54,7 +60,15 @@ public void EmpPulse(MapCoordinates coordinates, float range, float energyConsum TryEmpEffects(uid, energyConsumption, duration); } - Spawn(EmpPulseEffectPrototype, coordinates); + + var empBlast = Spawn(EmpPulseEffectPrototype, coordinates); // Frontier: Added visual effect + EnsureComp(empBlast, out var empBlastComp); // Frontier + empBlastComp.VisualRange = range; // Frontier + + if (range > _cfg.GetCVar(CVars.NetMaxUpdateRange)) // Frontier + _pvs.AddGlobalOverride(empBlast); // Frontier + + Dirty(empBlast, empBlastComp); // Frontier } /// diff --git a/Content.Shared/_NF/Emp/Components/EmpBlastComponent.cs b/Content.Shared/_NF/Emp/Components/EmpBlastComponent.cs new file mode 100644 index 00000000000..ea58235bd61 --- /dev/null +++ b/Content.Shared/_NF/Emp/Components/EmpBlastComponent.cs @@ -0,0 +1,30 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared._NF.Emp.Components; + +/// +/// Create circle pulse animation of emp around object. +/// Drawn on client after creation only once per component lifetime. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class EmpBlastComponent : Component +{ + /// + /// Timestamp when component was assigned to this entity. + /// + [AutoNetworkedField] + public TimeSpan StartTime; + + /// + /// How long will animation play in seconds. + /// Can be overridden by . + /// + [DataField, AutoNetworkedField] + public float VisualDuration = 1f; + + /// + /// The range of animation. + /// + [DataField, AutoNetworkedField] + public float VisualRange = 5f; +} diff --git a/Content.Shared/_NF/Emp/Systems/EmpBlastSystem.cs b/Content.Shared/_NF/Emp/Systems/EmpBlastSystem.cs new file mode 100644 index 00000000000..e5476ab693b --- /dev/null +++ b/Content.Shared/_NF/Emp/Systems/EmpBlastSystem.cs @@ -0,0 +1,27 @@ +using Content.Shared._NF.Emp.Components; +using Robust.Shared.Spawners; +using Robust.Shared.Timing; + +namespace Content.Shared._NF.Emp.Systems; + +public sealed class EmpBlastSystem : EntitySystem +{ + [Dependency] private readonly IGameTiming _timing = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnStartup); + } + + private void OnStartup(EntityUid uid, EmpBlastComponent component, ComponentStartup args) + { + component.StartTime = _timing.RealTime; + + // try to get despawn time or keep default duration time + if (TryComp(uid, out var despawn)) + { + component.VisualDuration = despawn.Lifetime; + } + } +} diff --git a/Resources/Prototypes/_NF/Entities/Effects/emp.yml b/Resources/Prototypes/_NF/Entities/Effects/emp.yml new file mode 100644 index 00000000000..cf0eb140c94 --- /dev/null +++ b/Resources/Prototypes/_NF/Entities/Effects/emp.yml @@ -0,0 +1,16 @@ +- type: entity + name: emp blast + id: EffectEmpBlast + description: Looking at this anomaly makes you feel an electric tingling all over your body. + categories: [ HideSpawnMenu ] + components: + - type: EmpBlast + - type: TimedDespawn + lifetime: 1 + - type: Tag + tags: + - HideContextMenu + - type: EmitSoundOnSpawn + sound: + path: /Audio/Effects/Lightning/lightningbolt.ogg + - type: AnimationPlayer \ No newline at end of file diff --git a/Resources/Prototypes/_NF/Shaders/shaders.yml b/Resources/Prototypes/_NF/Shaders/shaders.yml new file mode 100644 index 00000000000..828a6a6d604 --- /dev/null +++ b/Resources/Prototypes/_NF/Shaders/shaders.yml @@ -0,0 +1,7 @@ +- type: shader + id: Emp + kind: source + path: "/Textures/_NF/Shaders/emp.swsl" + params: + positionInput: 0,0 + life: 0 \ No newline at end of file diff --git a/Resources/Textures/_NF/Shaders/emp.swsl b/Resources/Textures/_NF/Shaders/emp.swsl new file mode 100644 index 00000000000..12f177b99e8 --- /dev/null +++ b/Resources/Textures/_NF/Shaders/emp.swsl @@ -0,0 +1,99 @@ +// From https://godotshaders.com/snippet/2d-noise/ + +uniform sampler2D SCREEN_TEXTURE; +uniform highp vec2 positionInput; +uniform highp vec2 renderScale; +uniform highp float life; +uniform highp float range; + +highp vec2 random(highp vec2 uv){ + uv = vec2( dot(uv, vec2(127.1,311.7) ), + dot(uv, vec2(269.5,183.3) ) ); + return -1.0 + 2.0 * fract(sin(uv) * 43758.5453123); +} + +highp float noise(highp vec2 uv) { + highp vec2 uv_index = floor(uv); + highp vec2 uv_fract = fract(uv); + + highp vec2 blur = smoothstep(0.0, 1.0, uv_fract); + + return mix( mix( dot( random(uv_index + vec2(0.0,0.0) ), uv_fract - vec2(0.0,0.0) ), + dot( random(uv_index + vec2(1.0,0.0) ), uv_fract - vec2(1.0,0.0) ), blur.x), + mix( dot( random(uv_index + vec2(0.0,1.0) ), uv_fract - vec2(0.0,1.0) ), + dot( random(uv_index + vec2(1.0,1.0) ), uv_fract - vec2(1.0,1.0) ), blur.x), blur.y) * 0.5 + 0.5; +} + +highp float fbm(highp vec2 uv) { + const int octaves = 6; + highp float amplitude = 0.5; + highp float frequency = 3.0; + highp float value = 0.0; + + for(int i = 0; i < octaves; i++) { + value += amplitude * noise(frequency * uv); + amplitude *= 0.5; + frequency *= 2.0; + } + return value; +} + +void fragment() { + highp vec2 finalCoords = (FRAGCOORD.xy - positionInput) / (renderScale * 32.0); + highp float distanceToCenter = length(finalCoords); + highp float nlife = pow(sin(clamp(life, 0.0, 1.0) * 3.141592), 0.5); + highp float on = ((range - distanceToCenter) / range); + highp float n = on; + highp vec2 fcOffset = vec2(fbm(finalCoords.xy + life / 2.0),fbm(finalCoords.yx + life / 2.0)); + n *= fbm((finalCoords + fcOffset) / (nlife / (n * 1.5))) * 1.1; + n *= clamp(nlife, 0.0, 1.0); + highp float a = 0.0; // Alpha + highp float p = 0.0; // Position between L and R stops + lowp vec3 lCol = vec3(0.0); // Left stop color + lowp vec3 rCol = vec3(0.0); // Right stop color + + if (n <= 0.05) { + p = 0.0; + a = 0.0; + lCol = vec3(0.0); + rCol = vec3(0.0); + } else if (n < 0.132) { + p = (n - 0.05) / (0.132 - 0.05); + a = p; + lCol = vec3(0.0); + rCol = vec3(0.098, 0.112, 0.406); + } else if (n < 0.186) { + p = (n - 0.132) / (0.186 - 0.132); + a = 1.0; + lCol = vec3(0.098, 0.112, 0.406); + rCol = vec3(0.168, 0.288, 1.000); + } else if (n < 0.388) { + p = (n - 0.186) / (0.388 - 0.186); + a = 1.0; + lCol = vec3(0.168, 0.288, 1.000); + rCol = vec3(0.583, 0.640, 1.000); + } else if (n >= 0.388) { + p = (n - 0.388) / 0.5; + a = 1.0; + lCol = vec3(0.583, 0.640, 1.000); + rCol = vec3(1.000, 1.000, 1.000); + } + + p = clamp(p, 0.0, 1.0); + + highp vec4 warped = zTextureSpec(SCREEN_TEXTURE, (FRAGCOORD.xy*SCREEN_PIXEL_SIZE)+clamp(on*nlife*(fcOffset/8.0), 0.0, 1.0)); + + // Extremely hacky way to detect FoV cones + highp float osum = warped.r + warped.g + warped.b; + highp float osr = osum > 0.1 ? 1.0 : 10.0 * osum; + + // Apply overlay + // FYI: If you want a smoother mix, swap lCol and rCol. + warped += mix( + vec4(0.0), + vec4(mix(rCol, lCol, vec3(p)), a), + osr + ); + + COLOR = warped; +} \ No newline at end of file