Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EMP Blast #2262

Merged
merged 9 commits into from
Oct 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Content.Client/Entry/EntryPoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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();
Expand Down
145 changes: 145 additions & 0 deletions Content.Client/_NF/Emp/Overlays/EmpBlastOverlay.cs
Original file line number Diff line number Diff line change
@@ -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<EntityUid, (ShaderInstance shd, EmpShaderInstance instance)> _blasts = new();

public EmpBlastOverlay()
{
IoCManager.InjectDependencies(this);
_baseShader = _prototypeManager.Index<ShaderPrototype>("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<TransformSystem>();

if (currentEye == null)
{
_blasts.Clear();
return;
}

var currentEyeLoc = currentEye.Position;

var blasts = _entityManager.EntityQueryEnumerator<EmpBlastComponent>();
//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<TransformComponent>(blastEntity);
var transformSystem = _entityManager.System<SharedTransformSystem>();
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;
};
}
}

18 changes: 16 additions & 2 deletions Content.Server/Emp/EmpSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,21 @@
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;

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()
{
Expand Down Expand Up @@ -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<EmpBlastComponent>(empBlast, out var empBlastComp); // Frontier
empBlastComp.VisualRange = range; // Frontier

if (range > _cfg.GetCVar(CVars.NetMaxUpdateRange)) // Frontier
_pvs.AddGlobalOverride(empBlast); // Frontier

Dirty(empBlast, empBlastComp); // Frontier
}

/// <summary>
Expand Down
30 changes: 30 additions & 0 deletions Content.Shared/_NF/Emp/Components/EmpBlastComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using Robust.Shared.GameStates;

namespace Content.Shared._NF.Emp.Components;

/// <summary>
/// Create circle pulse animation of emp around object.
/// Drawn on client after creation only once per component lifetime.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class EmpBlastComponent : Component
{
/// <summary>
/// Timestamp when component was assigned to this entity.
/// </summary>
[AutoNetworkedField]
public TimeSpan StartTime;

/// <summary>
/// How long will animation play in seconds.
/// Can be overridden by <see cref="Robust.Shared.Spawners.TimedDespawnComponent"/>.
/// </summary>
[DataField, AutoNetworkedField]
public float VisualDuration = 1f;

/// <summary>
/// The range of animation.
/// </summary>
[DataField, AutoNetworkedField]
public float VisualRange = 5f;
}
27 changes: 27 additions & 0 deletions Content.Shared/_NF/Emp/Systems/EmpBlastSystem.cs
Original file line number Diff line number Diff line change
@@ -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<EmpBlastComponent, ComponentStartup>(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<TimedDespawnComponent>(uid, out var despawn))
{
component.VisualDuration = despawn.Lifetime;
}
}
}
16 changes: 16 additions & 0 deletions Resources/Prototypes/_NF/Entities/Effects/emp.yml
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions Resources/Prototypes/_NF/Shaders/shaders.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
- type: shader
id: Emp
kind: source
path: "/Textures/_NF/Shaders/emp.swsl"
params:
positionInput: 0,0
life: 0
99 changes: 99 additions & 0 deletions Resources/Textures/_NF/Shaders/emp.swsl
Original file line number Diff line number Diff line change
@@ -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;
}
Loading