diff --git a/Content.Client/_Sunrise/ThermalVision/ThermalVisionOverlay.cs b/Content.Client/_Sunrise/ThermalVision/ThermalVisionOverlay.cs new file mode 100644 index 0000000000..3a70b03f43 --- /dev/null +++ b/Content.Client/_Sunrise/ThermalVision/ThermalVisionOverlay.cs @@ -0,0 +1,61 @@ +using Content.Shared._Sunrise.ThermalVision; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Client.Player; +using Robust.Shared.Enums; +using Robust.Shared.Prototypes; + +namespace Content.Client._Sunrise.ThermalVision; + +public sealed class ThermalVisionOverlay : Overlay +{ + [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + private readonly TransformSystem _transform; + public override bool RequestScreenTexture => true; + public override OverlaySpace Space => OverlaySpace.WorldSpace; + private readonly ShaderInstance _screenShader; + public ThermalVisionOverlay() + { + IoCManager.InjectDependencies(this); + _screenShader = _prototypeManager.Index("ThermalVisionScreenShader").InstanceUnique(); + _transform = _entityManager.System(); + ZIndex = 10000; + } + + protected override bool BeforeDraw(in OverlayDrawArgs args) + { + if (!_entityManager.TryGetComponent(_playerManager.LocalSession?.AttachedEntity, out EyeComponent? eyeComp)) + return false; + + if (args.Viewport.Eye != eyeComp.Eye) + return false; + + var playerEntity = _playerManager.LocalSession?.AttachedEntity; + + if (playerEntity == null) + return false; + + if (!_entityManager.TryGetComponent(playerEntity, out var blurComp)) + return false; + + return true; + } + + protected override void Draw(in OverlayDrawArgs args) + { + if (ScreenTexture == null) + return; + + var worldHandle = args.WorldHandle; + var viewport = args.WorldBounds; + var eyeRotation = args.Viewport.Eye?.Rotation ?? Angle.Zero; + + _screenShader.SetParameter("SCREEN_TEXTURE", ScreenTexture); + + worldHandle.UseShader(_screenShader); + worldHandle.DrawRect(viewport, Color.White); + worldHandle.UseShader(null); + } +} diff --git a/Content.Client/_Sunrise/ThermalVision/ThermalVisionSystem.cs b/Content.Client/_Sunrise/ThermalVision/ThermalVisionSystem.cs new file mode 100644 index 0000000000..204d38cc92 --- /dev/null +++ b/Content.Client/_Sunrise/ThermalVision/ThermalVisionSystem.cs @@ -0,0 +1,83 @@ +using Content.Shared._Sunrise.ThermalVision; +using Content.Shared.Eye.Blinding.Components; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Client.Player; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; + +namespace Content.Client._Sunrise.ThermalVision; + +public sealed class ThermalVisionSystem : EntitySystem +{ + [Dependency] private readonly IPlayerManager _player = default!; + [Dependency] private readonly IOverlayManager _overlayMan = default!; + [Dependency] private readonly TransformSystem _xformSys = default!; + private ThroughWallsVisionOverlay _throughWallsOverlay = default!; + private ThermalVisionOverlay _overlay = default!; + + private EntityUid? _effect = null; + private readonly EntProtoId _effectPrototype = "EffectThermalVision"; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnVisionInit); + SubscribeLocalEvent(OnVisionShutdown); + + SubscribeLocalEvent(OnPlayerAttached); + SubscribeLocalEvent(OnPlayerDetached); + + _throughWallsOverlay = new(); + _overlay = new(); + } + + private void OnPlayerAttached(Entity ent, ref LocalPlayerAttachedEvent args) + { + if (_effect == null) + AddNightVision(ent.Owner); + else if (HasComp(ent.Owner)) + RemoveNightVision(); + } + + private void OnPlayerDetached(Entity ent, ref LocalPlayerDetachedEvent args) + { + RemoveNightVision(); + } + + private void OnVisionInit(Entity ent, ref ComponentInit args) + { + if (_player.LocalEntity != ent.Owner) + return; + + if (_effect == null) + AddNightVision(ent.Owner); + else if (HasComp(ent.Owner)) + RemoveNightVision(); + } + + private void OnVisionShutdown(Entity ent, ref ComponentShutdown args) + { + if (_player.LocalEntity != ent.Owner) return; + + RemoveNightVision(); + } + + private void AddNightVision(EntityUid uid) + { + if (HasComp(uid)) return; + + _overlayMan.AddOverlay(_throughWallsOverlay); + _overlayMan.AddOverlay(_overlay); + _effect = SpawnAttachedTo(_effectPrototype, Transform(uid).Coordinates); + _xformSys.SetParent(_effect.Value, uid); + } + private void RemoveNightVision() + { + _overlayMan.RemoveOverlay(_throughWallsOverlay); + _overlayMan.RemoveOverlay(_overlay); + Del(_effect); + _effect = null; + } +} diff --git a/Content.Client/_Sunrise/ThermalVision/ThroughWallsVisionOverlay.cs b/Content.Client/_Sunrise/ThermalVision/ThroughWallsVisionOverlay.cs new file mode 100644 index 0000000000..e53b2f8135 --- /dev/null +++ b/Content.Client/_Sunrise/ThermalVision/ThroughWallsVisionOverlay.cs @@ -0,0 +1,71 @@ +using Content.Shared._Sunrise.ThermalVision; +using Content.Shared.Body.Components; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Client.Player; +using Robust.Shared.Enums; +using Robust.Shared.Prototypes; + +namespace Content.Client._Sunrise.ThermalVision; + +public sealed class ThroughWallsVisionOverlay : Overlay +{ + [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + private readonly ContainerSystem _containerSystem; + private readonly TransformSystem _transform; + private readonly ShaderInstance _shader; + + public override bool RequestScreenTexture => true; + public override OverlaySpace Space => OverlaySpace.WorldSpace; + public ThroughWallsVisionOverlay() + { + IoCManager.InjectDependencies(this); + _transform = _entityManager.System(); + _containerSystem = _entityManager.System(); + + _shader = _prototypeManager.Index("BrightnessShader").InstanceUnique(); + } + + protected override bool BeforeDraw(in OverlayDrawArgs args) + { + if (!_entityManager.TryGetComponent(_playerManager.LocalSession?.AttachedEntity, out EyeComponent? eyeComp)) + return false; + + if (args.Viewport.Eye != eyeComp.Eye) + return false; + + var playerEntity = _playerManager.LocalSession?.AttachedEntity; + + if (playerEntity == null) + return false; + + if (!_entityManager.TryGetComponent(playerEntity, out var blurComp)) + return false; + + return true; + } + + protected override void Draw(in OverlayDrawArgs args) + { + if (ScreenTexture == null) + return; + + var worldHandle = args.WorldHandle; + var viewport = args.WorldBounds; + var eyeRotation = args.Viewport.Eye?.Rotation ?? Angle.Zero; + + worldHandle.UseShader(_shader); + var query = _entityManager.EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out _, out var meta, out var sprite, out var xform)) + { + if (xform.MapID != args.MapId || _containerSystem.IsEntityInContainer(uid, meta)) continue; + var (position, rotation) = _transform.GetWorldPositionRotation(xform); + + sprite.Render(worldHandle, eyeRotation, rotation, null, position); + } + + worldHandle.UseShader(null); + } +} diff --git a/Content.Shared/_Sunrise/ThermalVision/ThermalVisionComponent.cs b/Content.Shared/_Sunrise/ThermalVision/ThermalVisionComponent.cs new file mode 100644 index 0000000000..874ff94786 --- /dev/null +++ b/Content.Shared/_Sunrise/ThermalVision/ThermalVisionComponent.cs @@ -0,0 +1,9 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared._Sunrise.ThermalVision; + +[RegisterComponent] +[NetworkedComponent] +public sealed partial class ThermalVisionComponent : Component +{ +} diff --git a/Resources/Changelog/ChangelogSunrise.yml b/Resources/Changelog/ChangelogSunrise.yml index 4c1b7b6f6f..20f86e2295 100644 --- a/Resources/Changelog/ChangelogSunrise.yml +++ b/Resources/Changelog/ChangelogSunrise.yml @@ -12015,3 +12015,14 @@ type: Fix id: 827 time: '2025-02-10T01:32:35.194700+00:00' +- author: VigersRay + changes: + - message: "\u0422\u0435\u0440\u043C\u0430\u043B\u044C\u043D\u044B\u0435 \u043E\u0447\ + \u043A\u0438 \u0442\u0435\u043F\u0435\u0440\u044C \u0440\u0430\u0431\u043E\u0442\ + \u0430\u044E\u0442 \u0438 \u043F\u043E\u0437\u0432\u043E\u043B\u044F\u044E\u0442\ + \ \u0432\u0438\u0434\u0435\u0442\u044C \u0441\u0443\u0449\u0435\u0441\u0442\u0432\ + \ \u0437\u0430 \u0441\u0442\u0435\u043D\u0430\u043C\u0438 \u0430 \u0442\u0430\ + \u043A \u0436\u0435 \u043D\u0435\u0432\u0438\u0434\u0438\u043C\u043E\u043A." + type: Tweak + id: 828 + time: '2025-02-10T04:43:25.594250+00:00' diff --git a/Resources/Prototypes/Entities/Clothing/Eyes/glasses.yml b/Resources/Prototypes/Entities/Clothing/Eyes/glasses.yml index c125891d5b..3ec1516d9f 100644 --- a/Resources/Prototypes/Entities/Clothing/Eyes/glasses.yml +++ b/Resources/Prototypes/Entities/Clothing/Eyes/glasses.yml @@ -223,7 +223,7 @@ #Make a scanner category when these actually function and we get the trayson - type: entity - parent: ClothingEyesBase + parent: [ClothingEyesBase, BaseToggleClothing] id: ClothingEyesGlassesThermal name: optical thermal scanner description: Thermals in the shape of glasses. @@ -239,6 +239,15 @@ - type: GroupExamine - type: IdentityBlocker coverage: EYES + # Sunrise-Start + - type: ToggleClothing + action: ActionToggleThermalVision + disableOnUnequip: true + - type: ComponentToggler + parent: true + components: + - type: ThermalVision + # Sunrise-End - type: entity parent: ClothingEyesBase diff --git a/Resources/Prototypes/_Sunrise/Actions/thermal.yml b/Resources/Prototypes/_Sunrise/Actions/thermal.yml new file mode 100644 index 0000000000..a143e8af8d --- /dev/null +++ b/Resources/Prototypes/_Sunrise/Actions/thermal.yml @@ -0,0 +1,9 @@ +- type: entity + id: ActionToggleThermalVision + name: toggle thermal vision + description: Toggle thermal vision on or off. + components: + - type: InstantAction + useDelay: 5 + priority: -9 + event: !type:ToggleActionEvent diff --git a/Resources/Prototypes/_Sunrise/Shaders/sharders.yml b/Resources/Prototypes/_Sunrise/Shaders/sharders.yml index 51eda3a463..201fbd3d75 100644 --- a/Resources/Prototypes/_Sunrise/Shaders/sharders.yml +++ b/Resources/Prototypes/_Sunrise/Shaders/sharders.yml @@ -10,3 +10,18 @@ id: NoMirageStealth kind: source path: "/Textures/_Sunrise/Shaders/no_mirage_stealth.swsl" + +- type: shader + id: ThermalVisionScreenShader + kind: source + path: "/Textures/_Sunrise/Shaders/thermal_screen.swsl" + params: + ThermalBoost: 1.5 + ThermalIntensity: 1 + +- type: shader + id: BrightnessShader + kind: source + path: "/Textures/_Sunrise/Shaders/brightness.swsl" + params: + brightness: 1.5 diff --git a/Resources/Prototypes/_Sunrise/ThermalVision/light.yml b/Resources/Prototypes/_Sunrise/ThermalVision/light.yml new file mode 100644 index 0000000000..834d333c14 --- /dev/null +++ b/Resources/Prototypes/_Sunrise/ThermalVision/light.yml @@ -0,0 +1,10 @@ +- type: entity + id: EffectThermalVision + categories: [ HideSpawnMenu ] + name: thermal vision + components: + - type: PointLight + color: "#FFE4D6" + energy: 1.8 + radius: 20 + softness: 20 diff --git a/Resources/Textures/_Sunrise/Shaders/brightness.swsl b/Resources/Textures/_Sunrise/Shaders/brightness.swsl new file mode 100644 index 0000000000..dfdfa2ecd1 --- /dev/null +++ b/Resources/Textures/_Sunrise/Shaders/brightness.swsl @@ -0,0 +1,8 @@ +uniform highp float brightness; + +void fragment() { + highp vec4 col = zTexture(UV); + col.rgb *= brightness; + COLOR = col; + lightSample = vec3(1.0); +} \ No newline at end of file diff --git a/Resources/Textures/_Sunrise/Shaders/thermal_screen.swsl b/Resources/Textures/_Sunrise/Shaders/thermal_screen.swsl new file mode 100644 index 0000000000..ceb20a69d2 --- /dev/null +++ b/Resources/Textures/_Sunrise/Shaders/thermal_screen.swsl @@ -0,0 +1,60 @@ +uniform sampler2D SCREEN_TEXTURE; + +uniform highp float ThermalBoost; +uniform highp float ThermalIntensity; + +const highp vec3 cBlack = vec3(0.00, 0.00, 0.00); +const highp vec3 cBlue = vec3(0.00, 0.00, 0.65); +const highp vec3 cGreen = vec3(0.00, 0.65, 0.00); +const highp vec3 cYellow = vec3(0.65, 0.65, 0.00); +const highp vec3 cRed = vec3(0.65, 0.00, 0.00); +const highp vec3 cWhite = vec3(0.65, 0.65, 0.65); + +highp vec3 getThermalColor(in highp float t) +{ + // 0.0..0.2: black -> blue + // 0.2..0.4: blue -> green + // 0.4..0.6: green -> yellow + // 0.6..0.8: yellow-> red + // 0.8..1.0: red -> white + + if (t < 0.2) + { + highp float alpha = t / 0.2; + return mix(cBlack, cBlue, alpha); + } + else if (t < 0.4) + { + highp float alpha = (t - 0.2) / 0.2; + return mix(cBlue, cGreen, alpha); + } + else if (t < 0.6) + { + highp float alpha = (t - 0.4) / 0.2; + return mix(cGreen, cYellow, alpha); + } + else if (t < 0.8) + { + highp float alpha = (t - 0.6) / 0.2; + return mix(cYellow, cRed, alpha); + } + else + { + highp float alpha = (t - 0.8) / 0.2; + return mix(cRed, cWhite, alpha); + } +} + +void fragment() +{ + COLOR = zTextureSpec(SCREEN_TEXTURE, Pos); + + highp float lum = dot(COLOR.rgb, vec3(0.299, 0.587, 0.114)); + + lum *= ThermalBoost; + lum = clamp(lum, 0.0, 1.0); + + highp vec3 thermalColor = getThermalColor(lum); + + COLOR.rgb = mix(COLOR.rgb, thermalColor, ThermalIntensity); +}