Skip to content

Commit

Permalink
Night And Thermal Vision (#1462)
Browse files Browse the repository at this point in the history
<!--
This is a semi-strict format, you can add/remove sections as needed but
the order/format should be kept the same
Remove these comments before submitting
-->

# Description

<!--
Explain this PR in as much detail as applicable

Some example prompts to consider:
How might this affect the game? The codebase?
What might be some alternatives to this?
How/Who does this benefit/hurt [the game/codebase]?
-->

Port from WWDP. Refactor from
[Goob](Goob-Station/Goob-Station#1251)

---

<!--
This is default collapsed, readers click to expand it and see all your
media
The PR media section can get very large at times, so this is a good way
to keep it clean
The title is written using HTML tags
The title must be within the <summary> tags or you won't see it
-->

<details><summary><h1>Media</h1></summary>
<p>


![image](https://github.com/user-attachments/assets/ee60eb59-0432-477e-8aee-25a56032e58e)

Night vision goggles:

![image](https://github.com/user-attachments/assets/f9269e1a-97ed-456f-bd45-7015b723fb3e)

Zealot's blindfold:

![image](https://github.com/user-attachments/assets/7c60011e-1d0a-4fb2-92cb-fded8c747555)

Animal vision:

![image](https://github.com/user-attachments/assets/14f1153d-b771-4316-9faa-fee951d884ce)

Thermal vision goggles:

![image](https://github.com/user-attachments/assets/b167ef8b-e1b7-477e-a08d-b217fd2e38c5)

Deathsquad helmet:

![image](https://github.com/user-attachments/assets/2e15ab15-6d23-45c2-b51e-3c16dc3a135d)

Xeno vision:

![image](https://github.com/user-attachments/assets/1677b69e-013f-464a-baaf-af3b5f1b6488)


</p>
</details>

---

# Changelog

<!--
You can add an author after the `:cl:` to change the name that appears
in the changelog (ex: `:cl: Death`)
Leaving it blank will default to your GitHub display name
This includes all available types for the changelog
-->

:cl: @Aviu00, Spatison, @PuroSlavKing
- add: Added night vision goggle
- add: Added thermal vision goggle
- add: Deathsquad helmet now grants night and thermal vision.
- add: Ninja visor now grants night vision.
- tweak: Some animals have gained night vision.
- tweak: Xenos have gained night vision.

---------

Signed-off-by: Spatison <[email protected]>
Co-authored-by: PuroSlavKing <[email protected]>
  • Loading branch information
Spatison and PuroSlavKing authored Jan 10, 2025
1 parent ba616f0 commit 0f48142
Show file tree
Hide file tree
Showing 81 changed files with 2,192 additions and 818 deletions.
2 changes: 1 addition & 1 deletion Content.Client/Overlays/EquipmentHudSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ protected virtual void OnRefreshComponentHud(EntityUid uid, T component, Refresh
args.Components.Add(component);
}

private void RefreshOverlay(EntityUid uid)
protected void RefreshOverlay(EntityUid uid)
{
if (uid != _player.LocalSession?.AttachedEntity)
return;
Expand Down
48 changes: 48 additions & 0 deletions Content.Client/Overlays/Switchable/BaseSwitchableOverlay.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using System.Numerics;
using Content.Shared.Overlays.Switchable;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
using Robust.Shared.Prototypes;

namespace Content.Client.Overlays.Switchable;

public sealed class BaseSwitchableOverlay<TComp> : Overlay where TComp : SwitchableOverlayComponent
{
[Dependency] private readonly IPrototypeManager _prototype = default!;

public override bool RequestScreenTexture => true;
public override OverlaySpace Space => OverlaySpace.WorldSpace;

private readonly ShaderInstance _shader;

public TComp? Comp = null;

public bool IsActive = true;

public BaseSwitchableOverlay()
{
IoCManager.InjectDependencies(this);
_shader = _prototype.Index<ShaderPrototype>("NightVision").InstanceUnique();
}

protected override void Draw(in OverlayDrawArgs args)
{
if (ScreenTexture is null || Comp is null || !IsActive)
return;

_shader.SetParameter("SCREEN_TEXTURE", ScreenTexture);
_shader.SetParameter("tint", Comp.Tint);
_shader.SetParameter("luminance_threshold", Comp.Strength);
_shader.SetParameter("noise_amount", Comp.Noise);

var worldHandle = args.WorldHandle;

var accumulator = Math.Clamp(Comp.PulseAccumulator, 0f, Comp.PulseTime);
var alpha = Comp.PulseTime <= 0f ? 1f : float.Lerp(1f, 0f, accumulator / Comp.PulseTime);

worldHandle.SetTransform(Matrix3x2.Identity);
worldHandle.UseShader(_shader);
worldHandle.DrawRect(args.WorldBounds, Comp.Color.WithAlpha(alpha));
worldHandle.UseShader(null);
}
}
87 changes: 87 additions & 0 deletions Content.Client/Overlays/Switchable/NightVisionSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
using Content.Shared.Inventory.Events;
using Content.Shared.Overlays.Switchable;
using Robust.Client.Graphics;

namespace Content.Client.Overlays.Switchable;

public sealed class NightVisionSystem : EquipmentHudSystem<NightVisionComponent>
{
[Dependency] private readonly IOverlayManager _overlayMan = default!;
[Dependency] private readonly ILightManager _lightManager = default!;

private BaseSwitchableOverlay<NightVisionComponent> _overlay = default!;

public override void Initialize()
{
base.Initialize();

SubscribeLocalEvent<NightVisionComponent, SwitchableOverlayToggledEvent>(OnToggle);

_overlay = new BaseSwitchableOverlay<NightVisionComponent>();
}

private void OnToggle(Entity<NightVisionComponent> ent, ref SwitchableOverlayToggledEvent args)
{
RefreshOverlay(args.User);
}

protected override void UpdateInternal(RefreshEquipmentHudEvent<NightVisionComponent> args)
{
base.UpdateInternal(args);

var active = false;
NightVisionComponent? nvComp = null;
foreach (var comp in args.Components)
{
if (comp.IsActive || comp.PulseTime > 0f && comp.PulseAccumulator < comp.PulseTime)
active = true;
else
continue;

if (comp.DrawOverlay)
{
if (nvComp == null)
nvComp = comp;
else if (nvComp.PulseTime > 0f && comp.PulseTime <= 0f)
nvComp = comp;
}

if (active && nvComp is { PulseTime: <= 0 })
break;
}

UpdateNightVision(active);
UpdateOverlay(nvComp);
}

protected override void DeactivateInternal()
{
base.DeactivateInternal();

UpdateNightVision(false);
UpdateOverlay(null);
}

private void UpdateNightVision(bool active)
{
_lightManager.DrawLighting = !active;
}

private void UpdateOverlay(NightVisionComponent? nvComp)
{
_overlay.Comp = nvComp;

switch (nvComp)
{
case not null when !_overlayMan.HasOverlay<BaseSwitchableOverlay<NightVisionComponent>>():
_overlayMan.AddOverlay(_overlay);
break;
case null:
_overlayMan.RemoveOverlay(_overlay);
break;
}

if (_overlayMan.TryGetOverlay<BaseSwitchableOverlay<ThermalVisionComponent>>(out var overlay))
overlay.IsActive = nvComp == null;
}
}
159 changes: 159 additions & 0 deletions Content.Client/Overlays/Switchable/ThermalVisionOverlay.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
using System.Linq;
using System.Numerics;
using Content.Client.Stealth;
using Content.Shared.Body.Components;
using Content.Shared.Overlays.Switchable;
using Content.Shared.Stealth.Components;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Shared.Enums;
using Robust.Shared.Map;
using Robust.Shared.Timing;

namespace Content.Client.Overlays.Switchable;

public sealed class ThermalVisionOverlay : Overlay
{
[Dependency] private readonly IEntityManager _entity = default!;
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IGameTiming _timing = default!;

private readonly TransformSystem _transform;
private readonly StealthSystem _stealth;
private readonly ContainerSystem _container;
private readonly SharedPointLightSystem _light;

public override bool RequestScreenTexture => true;
public override OverlaySpace Space => OverlaySpace.WorldSpace;

private readonly List<ThermalVisionRenderEntry> _entries = [];

private EntityUid? _lightEntity;

public float LightRadius;

public ThermalVisionComponent? Comp;

public ThermalVisionOverlay()
{
IoCManager.InjectDependencies(this);

_container = _entity.System<ContainerSystem>();
_transform = _entity.System<TransformSystem>();
_stealth = _entity.System<StealthSystem>();
_light = _entity.System<SharedPointLightSystem>();

ZIndex = -1;
}

protected override void Draw(in OverlayDrawArgs args)
{
if (ScreenTexture is null || Comp is null)
return;

var worldHandle = args.WorldHandle;
var eye = args.Viewport.Eye;

if (eye == null)
return;

var player = _player.LocalEntity;

if (!_entity.TryGetComponent(player, out TransformComponent? playerXform))
return;

var accumulator = Math.Clamp(Comp.PulseAccumulator, 0f, Comp.PulseTime);
var alpha = Comp.PulseTime <= 0f ? 1f : float.Lerp(1f, 0f, accumulator / Comp.PulseTime);

// Thermal vision grants some night vision (clientside light)
if (LightRadius > 0)
{
_lightEntity ??= _entity.SpawnAttachedTo(null, playerXform.Coordinates);
_transform.SetParent(_lightEntity.Value, player.Value);
var light = _entity.EnsureComponent<PointLightComponent>(_lightEntity.Value);
_light.SetRadius(_lightEntity.Value, LightRadius, light);
_light.SetEnergy(_lightEntity.Value, alpha, light);
_light.SetColor(_lightEntity.Value, Comp.Color, light);
}
else
ResetLight();

var mapId = eye.Position.MapId;
var eyeRot = eye.Rotation;

_entries.Clear();
var entities = _entity.EntityQueryEnumerator<BodyComponent, SpriteComponent, TransformComponent>();
while (entities.MoveNext(out var uid, out var body, out var sprite, out var xform))
{
if (!CanSee(uid, sprite) || !body.ThermalVisibility)
continue;

var entity = uid;

if (_container.TryGetOuterContainer(uid, xform, out var container))
{
var owner = container.Owner;
if (_entity.TryGetComponent<SpriteComponent>(owner, out var ownerSprite)
&& _entity.TryGetComponent<TransformComponent>(owner, out var ownerXform))
{
entity = owner;
sprite = ownerSprite;
xform = ownerXform;
}
}

if (_entries.Any(e => e.Ent.Owner == entity))
continue;

_entries.Add(new ThermalVisionRenderEntry((entity, sprite, xform), mapId, eyeRot));
}

foreach (var entry in _entries)
{
Render(entry.Ent, entry.Map, worldHandle, entry.EyeRot, Comp.Color, alpha);
}

worldHandle.SetTransform(Matrix3x2.Identity);
}

private void Render(Entity<SpriteComponent, TransformComponent> ent,
MapId? map,
DrawingHandleWorld handle,
Angle eyeRot,
Color color,
float alpha)
{
var (uid, sprite, xform) = ent;
if (xform.MapID != map || !CanSee(uid, sprite))
return;

var position = _transform.GetWorldPosition(xform);
var rotation = _transform.GetWorldRotation(xform);

var originalColor = sprite.Color;
sprite.Color = color.WithAlpha(alpha);
sprite.Render(handle, eyeRot, rotation, position: position);
sprite.Color = originalColor;
}

private bool CanSee(EntityUid uid, SpriteComponent sprite)
{
return sprite.Visible && (!_entity.TryGetComponent(uid, out StealthComponent? stealth) ||
_stealth.GetVisibility(uid, stealth) > 0.5f);
}

public void ResetLight()
{
if (_lightEntity == null || !_timing.IsFirstTimePredicted)
return;

_entity.DeleteEntity(_lightEntity);
_lightEntity = null;
}
}

public record struct ThermalVisionRenderEntry(
Entity<SpriteComponent, TransformComponent> Ent,
MapId? Map,
Angle EyeRot);
Loading

0 comments on commit 0f48142

Please sign in to comment.