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;
+}