diff --git a/Content.Client/StationEvents/RadiationPulseComponent.cs b/Content.Client/StationEvents/RadiationPulseComponent.cs index 00b0f5bdd2..065ec46599 100644 --- a/Content.Client/StationEvents/RadiationPulseComponent.cs +++ b/Content.Client/StationEvents/RadiationPulseComponent.cs @@ -12,10 +12,12 @@ namespace Content.Client.StationEvents private bool _decay; private float _radsPerSecond; private float _range; + private TimeSpan _startTime; private TimeSpan _endTime; public override float RadsPerSecond => _radsPerSecond; public override float Range => _range; + public override TimeSpan StartTime => _startTime; public override TimeSpan EndTime => _endTime; public override bool Draw => _draw; public override bool Decay => _decay; @@ -33,6 +35,7 @@ namespace Content.Client.StationEvents _range = state.Range; _draw = state.Draw; _decay = state.Decay; + _startTime = state.StartTime; _endTime = state.EndTime; } } diff --git a/Content.Client/StationEvents/RadiationPulseOverlay.cs b/Content.Client/StationEvents/RadiationPulseOverlay.cs index a556b95fb0..6bef4d2dd6 100644 --- a/Content.Client/StationEvents/RadiationPulseOverlay.cs +++ b/Content.Client/StationEvents/RadiationPulseOverlay.cs @@ -1,152 +1,138 @@ using System; using System.Collections.Generic; using System.Linq; -using JetBrains.Annotations; using Robust.Client.Graphics; -using Robust.Client.Player; using Robust.Shared.Enums; using Robust.Shared.GameObjects; +using Robust.Shared.Timing; using Robust.Shared.IoC; using Robust.Shared.Map; using Robust.Shared.Maths; -using Robust.Shared.Timing; +using Robust.Shared.Prototypes; namespace Content.Client.StationEvents { - [UsedImplicitly] - public sealed class RadiationPulseOverlay : Overlay + public class RadiationPulseOverlay : Overlay { [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IGameTiming _gameTiming = default!; - [Dependency] private readonly IMapManager _mapManager = default!; - [Dependency] private readonly IPlayerManager _playerManager = default!; - [Dependency] private readonly IEyeManager _eyeManager = default!; - /// - /// Current color of a pulse - /// - private readonly Dictionary _colors = new(); - - /// - /// Whether our alpha is increasing or decreasing and at what time does it flip (or stop) - /// - private readonly Dictionary _transitions = - new(); - - /// - /// How much the alpha changes per second for each pulse - /// - private readonly Dictionary _alphaRateOfChange = new(); - - private TimeSpan _lastTick; + private const float MaxDist = 15.0f; public override OverlaySpace Space => OverlaySpace.WorldSpace; + public override bool RequestScreenTexture => true; + + private TimeSpan _lastTick = default; + + private readonly ShaderInstance _baseShader; + private readonly Dictionary _pulses = new(); public RadiationPulseOverlay() { IoCManager.InjectDependencies(this); - _lastTick = _gameTiming.CurTime; - } - - /// - /// Get the current color for the entity, - /// accounting for what its alpha should be and whether it should be transitioning in or out - /// - /// - /// frametime - /// - /// - private Color GetColor(IEntity entity, float elapsedTime, TimeSpan endTime) - { - var currentTime = _gameTiming.CurTime; - - // New pulse - if (!_colors.ContainsKey(entity)) - { - UpdateTransition(entity, currentTime, endTime); - } - - var currentColor = _colors[entity]; - var alphaChange = _alphaRateOfChange[entity] * elapsedTime; - - if (!_transitions[entity].EasingIn) - { - alphaChange *= -1; - } - - if (currentTime > _transitions[entity].TransitionTime) - { - UpdateTransition(entity, currentTime, endTime); - } - - _colors[entity] = _colors[entity].WithAlpha(currentColor.A + alphaChange); - return _colors[entity]; - } - - private void UpdateTransition(IEntity entity, TimeSpan currentTime, TimeSpan endTime) - { - bool easingIn; - TimeSpan transitionTime; - - if (!_transitions.TryGetValue(entity, out var transition)) - { - // Start as false because it will immediately be flipped - easingIn = false; - transitionTime = (endTime - currentTime) / 2 + currentTime; - } - else - { - easingIn = transition.EasingIn; - transitionTime = endTime; - } - - _transitions[entity] = (!easingIn, transitionTime); - _colors[entity] = Color.LimeGreen.WithAlpha(0.0f); - _alphaRateOfChange[entity] = 1.0f / (float) (transitionTime - currentTime).TotalSeconds; + _baseShader = _prototypeManager.Index("Radiation").Instance().Duplicate(); } protected override void Draw(in OverlayDrawArgs args) { - // PVS should control the overlay pretty well so the overlay doesn't get instantiated unless we're near one... - var playerEntity = _playerManager.LocalPlayer?.ControlledEntity; + RadiationQuery(args.Viewport.Eye); - if (playerEntity == null) - { + if (_pulses.Count == 0) return; - } - var radiationPulses = _entityManager - .EntityQuery(true) - .ToList(); - - if (radiationPulses.Count == 0) - { + if (ScreenTexture == null) return; - } - - var elapsedTime = (float) (_gameTiming.CurTime - _lastTick).TotalSeconds; - _lastTick = _gameTiming.CurTime; var worldHandle = args.WorldHandle; - var viewport = _eyeManager.GetWorldViewport(); - foreach (var grid in _mapManager.FindGridsIntersecting(playerEntity.Transform.MapID, viewport)) + var viewport = args.Viewport; + + foreach ((var shd, var instance) in _pulses.Values) { - worldHandle.SetTransform(grid.WorldMatrix); - foreach (var pulse in radiationPulses) - { - if (!pulse.Draw || grid.Index != pulse.Owner.Transform.GridID) 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); + tempCoords.Y = viewport.Size.Y - tempCoords.Y; + shd?.SetParameter("renderScale", viewport.RenderScale); + shd?.SetParameter("positionInput", tempCoords); + shd?.SetParameter("range", instance.Range); + var life = (_lastTick - instance.Start) / (instance.End - instance.Start); + shd?.SetParameter("life", (float) life); - var pulseTransform = pulse.Owner.Transform; + // There's probably a very good reason not to do this. + // Oh well! + shd?.SetParameter("SCREEN_TEXTURE", viewport.RenderTarget.Texture); - var maxVisibleCircleArea = viewport.Enlarged(pulse.Range * 64); - if (!maxVisibleCircleArea.Contains(pulseTransform.WorldPosition)) continue; - - worldHandle.DrawCircle( - pulseTransform.LocalPosition, - pulse.Range, - GetColor(pulse.Owner, pulse.Decay ? elapsedTime : 0, pulse.EndTime)); - } + worldHandle.UseShader(shd); + worldHandle.DrawRect(Box2.CenteredAround(instance.CurrentMapCoords, new Vector2(instance.Range, instance.Range) * 2f), Color.White); } } + + //Queries all pulses on the map and either adds or removes them from the list of rendered pulses based on whether they should be drawn (in range? on the same z-level/map? pulse entity still exists?) + private void RadiationQuery(IEye? currentEye) + { + if (currentEye == null) + { + _pulses.Clear(); + return; + } + + _lastTick = _gameTiming.CurTime; + + var currentEyeLoc = currentEye.Position; + + var pulses = _entityManager.EntityQuery(); + foreach (var pulse in pulses) //Add all pulses that are not added yet but qualify + { + var pulseEntity = pulse.Owner; + + if (!_pulses.Keys.Contains(pulseEntity.Uid) && PulseQualifies(pulseEntity, currentEyeLoc)) + { + _pulses.Add( + pulseEntity.Uid, + ( + _baseShader.Duplicate(), + new RadiationShaderInstance( + pulseEntity.Transform.MapPosition.Position, + pulse.Range, + pulse.StartTime, + pulse.EndTime + ) + ) + ); + } + } + + var activeShaderIds = _pulses.Keys; + foreach (var activePulseUid in activeShaderIds) //Remove all pulses that are added and no longer qualify + { + if (_entityManager.TryGetEntity(activePulseUid, out var pulseEntity) && + PulseQualifies(pulseEntity, currentEyeLoc) && + pulseEntity.TryGetComponent(out var pulse)) + { + var shaderInstance = _pulses[activePulseUid]; + shaderInstance.instance.CurrentMapCoords = pulseEntity.Transform.MapPosition.Position; + shaderInstance.instance.Range = pulse.Range; + } else { + _pulses[activePulseUid].shd.Dispose(); + _pulses.Remove(activePulseUid); + } + } + + } + + private bool PulseQualifies(IEntity pulseEntity, MapCoordinates currentEyeLoc) + { + return pulseEntity.Transform.MapID == currentEyeLoc.MapId && pulseEntity.Transform.Coordinates.InRange(_entityManager, EntityCoordinates.FromMap(_entityManager, pulseEntity.Transform.ParentUid, currentEyeLoc), MaxDist); + } + + private sealed record RadiationShaderInstance(Vector2 CurrentMapCoords, float Range, TimeSpan Start, TimeSpan End) + { + public Vector2 CurrentMapCoords = CurrentMapCoords; + public float Range = Range; + public TimeSpan Start = Start; + public TimeSpan End = End; + }; } } + diff --git a/Content.Server/Radiation/RadiationPulseComponent.cs b/Content.Server/Radiation/RadiationPulseComponent.cs index 6160af7027..7c1828bdf0 100644 --- a/Content.Server/Radiation/RadiationPulseComponent.cs +++ b/Content.Server/Radiation/RadiationPulseComponent.cs @@ -22,6 +22,7 @@ namespace Content.Server.Radiation private float _duration; private float _radsPerSecond = 8f; private float _range = 5f; + private TimeSpan _startTime; private TimeSpan _endTime; private bool _draw = true; private bool _decay = true; @@ -58,7 +59,7 @@ namespace Content.Server.Radiation } } - [DataField("sound")] public SoundSpecifier Sound { get; set; } = new SoundPathSpecifier("/Audio/Weapons/Guns/Gunshots/laser3.ogg"); + [DataField("sound")] public SoundSpecifier Sound { get; set; } = new SoundCollectionSpecifier("RadiationPulse"); [DataField("range")] public override float Range @@ -82,6 +83,7 @@ namespace Content.Server.Radiation } } + public override TimeSpan StartTime => _startTime; public override TimeSpan EndTime => _endTime; public void DoPulse() @@ -89,6 +91,7 @@ namespace Content.Server.Radiation if (Decay) { var currentTime = _gameTiming.CurTime; + _startTime = currentTime; _duration = _random.NextFloat() * (MaxPulseLifespan - MinPulseLifespan) + MinPulseLifespan; _endTime = currentTime + TimeSpan.FromSeconds(_duration); } @@ -100,7 +103,7 @@ namespace Content.Server.Radiation public override ComponentState GetComponentState() { - return new RadiationPulseState(_radsPerSecond, _range, Draw, Decay, _endTime); + return new RadiationPulseState(_radsPerSecond, _range, Draw, Decay, _startTime, _endTime); } public void Update(float frameTime) diff --git a/Content.Server/StationEvents/Events/RadiationStorm.cs b/Content.Server/StationEvents/Events/RadiationStorm.cs index 3b37e44584..725df773a3 100644 --- a/Content.Server/StationEvents/Events/RadiationStorm.cs +++ b/Content.Server/StationEvents/Events/RadiationStorm.cs @@ -92,6 +92,13 @@ namespace Content.Server.StationEvents.Events ResetTimeUntilPulse(); } + public static void SpawnPulseAt(EntityCoordinates at) + { + var pulse = IoCManager.Resolve() + .SpawnEntity("RadiationPulse", at); + pulse.GetComponent().DoPulse(); + } + private bool TryFindRandomGrid(IMapGrid mapGrid, out EntityCoordinates coordinates) { if (!mapGrid.Index.IsValid()) diff --git a/Content.Shared/Radiation/SharedRadiationStorm.cs b/Content.Shared/Radiation/SharedRadiationStorm.cs index 5b4b4a070f..f0b4783bfc 100644 --- a/Content.Shared/Radiation/SharedRadiationStorm.cs +++ b/Content.Shared/Radiation/SharedRadiationStorm.cs @@ -20,6 +20,7 @@ namespace Content.Shared.Radiation public virtual bool Decay { get; set; } public virtual bool Draw { get; set; } + public virtual TimeSpan StartTime { get; } public virtual TimeSpan EndTime { get; } } @@ -33,14 +34,16 @@ namespace Content.Shared.Radiation public readonly float Range; public readonly bool Draw; public readonly bool Decay; + public readonly TimeSpan StartTime; public readonly TimeSpan EndTime; - public RadiationPulseState(float radsPerSecond, float range, bool draw, bool decay, TimeSpan endTime) + public RadiationPulseState(float radsPerSecond, float range, bool draw, bool decay, TimeSpan startTime, TimeSpan endTime) { RadsPerSecond = radsPerSecond; Range = range; Draw = draw; Decay = decay; + StartTime = startTime; EndTime = endTime; } } diff --git a/Resources/Audio/Effects/radpulse1.ogg b/Resources/Audio/Effects/radpulse1.ogg new file mode 100644 index 0000000000..b4ad3a14d2 Binary files /dev/null and b/Resources/Audio/Effects/radpulse1.ogg differ diff --git a/Resources/Audio/Effects/radpulse10.ogg b/Resources/Audio/Effects/radpulse10.ogg new file mode 100644 index 0000000000..2a975fd7f3 Binary files /dev/null and b/Resources/Audio/Effects/radpulse10.ogg differ diff --git a/Resources/Audio/Effects/radpulse11.ogg b/Resources/Audio/Effects/radpulse11.ogg new file mode 100644 index 0000000000..63052122bf Binary files /dev/null and b/Resources/Audio/Effects/radpulse11.ogg differ diff --git a/Resources/Audio/Effects/radpulse12.ogg b/Resources/Audio/Effects/radpulse12.ogg new file mode 100644 index 0000000000..2d6fbd9c07 Binary files /dev/null and b/Resources/Audio/Effects/radpulse12.ogg differ diff --git a/Resources/Audio/Effects/radpulse2.ogg b/Resources/Audio/Effects/radpulse2.ogg new file mode 100644 index 0000000000..c36f9a5db0 Binary files /dev/null and b/Resources/Audio/Effects/radpulse2.ogg differ diff --git a/Resources/Audio/Effects/radpulse3.ogg b/Resources/Audio/Effects/radpulse3.ogg new file mode 100644 index 0000000000..ca671cc9f0 Binary files /dev/null and b/Resources/Audio/Effects/radpulse3.ogg differ diff --git a/Resources/Audio/Effects/radpulse4.ogg b/Resources/Audio/Effects/radpulse4.ogg new file mode 100644 index 0000000000..a2f318b6d5 Binary files /dev/null and b/Resources/Audio/Effects/radpulse4.ogg differ diff --git a/Resources/Audio/Effects/radpulse5.ogg b/Resources/Audio/Effects/radpulse5.ogg new file mode 100644 index 0000000000..0f9d0c28cc Binary files /dev/null and b/Resources/Audio/Effects/radpulse5.ogg differ diff --git a/Resources/Audio/Effects/radpulse6.ogg b/Resources/Audio/Effects/radpulse6.ogg new file mode 100644 index 0000000000..8ef5baacd8 Binary files /dev/null and b/Resources/Audio/Effects/radpulse6.ogg differ diff --git a/Resources/Audio/Effects/radpulse7.ogg b/Resources/Audio/Effects/radpulse7.ogg new file mode 100644 index 0000000000..84b2626aeb Binary files /dev/null and b/Resources/Audio/Effects/radpulse7.ogg differ diff --git a/Resources/Audio/Effects/radpulse8.ogg b/Resources/Audio/Effects/radpulse8.ogg new file mode 100644 index 0000000000..a0a1df41c0 Binary files /dev/null and b/Resources/Audio/Effects/radpulse8.ogg differ diff --git a/Resources/Audio/Effects/radpulse9.ogg b/Resources/Audio/Effects/radpulse9.ogg new file mode 100644 index 0000000000..658062151c Binary files /dev/null and b/Resources/Audio/Effects/radpulse9.ogg differ diff --git a/Resources/Prototypes/Shaders/shaders.yml b/Resources/Prototypes/Shaders/shaders.yml index a26a7c9c02..e6e12fd332 100644 --- a/Resources/Prototypes/Shaders/shaders.yml +++ b/Resources/Prototypes/Shaders/shaders.yml @@ -27,6 +27,14 @@ falloff: 5 intensity: 5 +- type: shader + id: Radiation + kind: source + path: "/Textures/Shaders/radiation.swsl" + params: + positionInput: 0,0 + life: 0 + - type: shader id: Texture kind: source diff --git a/Resources/Prototypes/SoundCollections/radiation.yml b/Resources/Prototypes/SoundCollections/radiation.yml new file mode 100644 index 0000000000..e8fb5cbc7d --- /dev/null +++ b/Resources/Prototypes/SoundCollections/radiation.yml @@ -0,0 +1,15 @@ +- type: soundCollection + id: RadiationPulse + files: + - /Audio/Effects/radpulse1.ogg + - /Audio/Effects/radpulse2.ogg + - /Audio/Effects/radpulse3.ogg + - /Audio/Effects/radpulse4.ogg + - /Audio/Effects/radpulse5.ogg + - /Audio/Effects/radpulse6.ogg + - /Audio/Effects/radpulse7.ogg + - /Audio/Effects/radpulse8.ogg + - /Audio/Effects/radpulse9.ogg + - /Audio/Effects/radpulse10.ogg + - /Audio/Effects/radpulse11.ogg + - /Audio/Effects/radpulse12.ogg diff --git a/Resources/Textures/Shaders/radiation.swsl b/Resources/Textures/Shaders/radiation.swsl new file mode 100644 index 0000000000..b28753a6d7 --- /dev/null +++ b/Resources/Textures/Shaders/radiation.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) { + 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(life * 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.406, 0.112); + } else if (n < 0.186) { + p = (n - 0.132) / (0.186 - 0.132); + a = 1.0; + lCol = vec3(0.098, 0.406, 0.112); + rCol = vec3(0.168, 1.000, 0.288); + } else if (n < 0.388) { + p = (n - 0.186) / (0.388 - 0.186); + a = 1.0; + lCol = vec3(0.168, 1.000, 0.288); + rCol = vec3(0.583, 1.000, 0.640); + } else if (n >= 0.388) { + p = (n - 0.388) / 0.5; + a = 1.0; + lCol = vec3(0.583, 1.000, 0.640); + 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 * 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; +}