From: Quantum-cross <7065792+Quantum-cross@users.noreply.github.com> Date: Thu, 4 Sep 2025 19:11:03 +0000 (-0400) Subject: Dynamic anomaly scanner texture (#37585) X-Git-Url: https://git.smokeofanarchy.ru/gitweb.cgi?a=commitdiff_plain;h=52c903cab85aa73502e439c73c7808d19d6570dc;p=space-station-14.git Dynamic anomaly scanner texture (#37585) --- diff --git a/Content.Client/Anomaly/AnomalyScannerScreenComponent.cs b/Content.Client/Anomaly/AnomalyScannerScreenComponent.cs new file mode 100644 index 0000000000..8e0b911fb7 --- /dev/null +++ b/Content.Client/Anomaly/AnomalyScannerScreenComponent.cs @@ -0,0 +1,40 @@ +using Robust.Client.Graphics; +using SixLabors.ImageSharp.PixelFormats; + +namespace Content.Client.Anomaly; + +/// +/// This component creates and handles the drawing of a ScreenTexture to be used on the Anomaly Scanner +/// for an indicator of Anomaly Severity. +/// +/// +/// In the future I would like to make this a more generic "DynamicTextureComponent" that can contain a dictionary +/// of texture components like "Bar(offset, size, minimumValue, maximumValue, AppearanceKey, LayerMapKey)" that can +/// just draw a bar or other basic drawn element that will show up on a texture layer. +/// +[RegisterComponent] +[Access(typeof(AnomalyScannerSystem))] +public sealed partial class AnomalyScannerScreenComponent : Component +{ + /// + /// This is the texture drawn as a layer on the Anomaly Scanner device. + /// + public OwnedTexture? ScreenTexture; + + /// + /// A small buffer that we can reuse to draw the severity bar. + /// + public Rgba32[]? BarBuf; + + /// + /// The position of the top-left of the severity bar in pixels. + /// + [DataField(readOnly: true)] + public Vector2i Offset = new Vector2i(12, 17); + + /// + /// The width and height of the severity bar in pixels. + /// + [DataField(readOnly: true)] + public Vector2i Size = new Vector2i(10, 3); +} diff --git a/Content.Client/Anomaly/AnomalyScannerSystem.cs b/Content.Client/Anomaly/AnomalyScannerSystem.cs new file mode 100644 index 0000000000..f80e5ead54 --- /dev/null +++ b/Content.Client/Anomaly/AnomalyScannerSystem.cs @@ -0,0 +1,110 @@ +using System.Numerics; +using Content.Shared.Anomaly; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Shared.Utility; +using SixLabors.ImageSharp.PixelFormats; + +namespace Content.Client.Anomaly; + +/// +public sealed class AnomalyScannerSystem : SharedAnomalyScannerSystem +{ + [Dependency] private readonly IClyde _clyde = default!; + [Dependency] private readonly SpriteSystem _sprite = default!; + + private const float MaxHueDegrees = 360f; + private const float GreenHueDegrees = 110f; + private const float RedHueDegrees = 0f; + private const float GreenHue = GreenHueDegrees / MaxHueDegrees; + private const float RedHue = RedHueDegrees / MaxHueDegrees; + + + // Just an array to initialize the pixels of a new OwnedTexture + private static readonly Rgba32[] EmptyTexture = new Rgba32[32*32]; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnComponentInit); + SubscribeLocalEvent(OnComponentStartup); + SubscribeLocalEvent(OnScannerAppearanceChanged); + } + + private void OnComponentInit(Entity ent, ref ComponentInit args) + { + if(!_sprite.TryGetLayer(ent.Owner, AnomalyScannerVisualLayers.Base, out var layer, true)) + return; + + // Allocate the OwnedTexture + ent.Comp.ScreenTexture = _clyde.CreateBlankTexture(layer.PixelSize); + + if (layer.PixelSize.X < ent.Comp.Offset.X + ent.Comp.Size.X || + layer.PixelSize.Y < ent.Comp.Offset.Y + ent.Comp.Size.Y) + { + // If the bar doesn't fit, just bail here, ScreenTexture and BarBuf will remain null, and appearance updates + // will do nothing. + DebugTools.Assert(false, "AnomalyScannerScreenComponent: Bar does not fit within sprite"); + return; + } + + + // Initialize the texture + ent.Comp.ScreenTexture.SetSubImage((0, 0), layer.PixelSize, new ReadOnlySpan(EmptyTexture)); + + // Initialize bar drawing buffer + ent.Comp.BarBuf = new Rgba32[ent.Comp.Size.X * ent.Comp.Size.Y]; + } + + private void OnComponentStartup(Entity ent, ref ComponentStartup args) + { + if (!TryComp(ent, out var sprite)) + return; + + _sprite.LayerSetTexture((ent, sprite), AnomalyScannerVisualLayers.Screen, ent.Comp.ScreenTexture); + } + + private void OnScannerAppearanceChanged(Entity ent, ref AppearanceChangeEvent args) + { + if (args.Sprite is null || ent.Comp.ScreenTexture is null || ent.Comp.BarBuf is null) + return; + + args.AppearanceData.TryGetValue(AnomalyScannerVisuals.AnomalySeverity, out var severityObj); + if (severityObj is not float severity) + severity = 0; + + // Get the bar length + var barLength = (int)(severity * ent.Comp.Size.X); + + // Calculate the bar color + // Hue "angle" of two colors to interpolate between depending on severity + // Just a lerp from Green hue at severity = 0.5 to Red hue at 1.0 + var hue = Math.Clamp(2*GreenHue * (1 - severity), RedHue, GreenHue); + var color = new Rgba32(Color.FromHsv(new Vector4(hue, 1f, 1f, 1f)).RGBA); + + var transparent = new Rgba32(0, 0, 0, 255); + + for(var y = 0; y < ent.Comp.Size.Y; y++) + { + for (var x = 0; x < ent.Comp.Size.X; x++) + { + ent.Comp.BarBuf[y*ent.Comp.Size.X + x] = x < barLength ? color : transparent; + } + } + + // Copy the buffer to the texture + try + { + ent.Comp.ScreenTexture.SetSubImage( + ent.Comp.Offset, + ent.Comp.Size, + new ReadOnlySpan(ent.Comp.BarBuf) + ); + } + catch (IndexOutOfRangeException) + { + Log.Warning($"Bar dimensions out of bounds with the texture on entity {ent.Owner}"); + } + } +} diff --git a/Content.Client/Anomaly/AnomalySystem.cs b/Content.Client/Anomaly/AnomalySystem.cs index 4eee43fac6..b4bc6efdd2 100644 --- a/Content.Client/Anomaly/AnomalySystem.cs +++ b/Content.Client/Anomaly/AnomalySystem.cs @@ -7,7 +7,7 @@ using Robust.Shared.Timing; namespace Content.Client.Anomaly; -public sealed class AnomalySystem : SharedAnomalySystem +public sealed partial class AnomalySystem : SharedAnomalySystem { [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly FloatingVisualizerSystem _floating = default!; @@ -24,6 +24,7 @@ public sealed class AnomalySystem : SharedAnomalySystem SubscribeLocalEvent(OnShutdown); } + private void OnStartup(EntityUid uid, AnomalyComponent component, ComponentStartup args) { _floating.FloatAnimation(uid, component.FloatingOffset, component.AnimationKey, component.AnimationTime); diff --git a/Content.Server/Anomaly/AnomalyScannerSystem.cs b/Content.Server/Anomaly/AnomalyScannerSystem.cs new file mode 100644 index 0000000000..ba657cf056 --- /dev/null +++ b/Content.Server/Anomaly/AnomalyScannerSystem.cs @@ -0,0 +1,185 @@ +using Content.Server.Anomaly.Components; +using Content.Server.Anomaly.Effects; +using Content.Shared.Anomaly; +using Content.Shared.Anomaly.Components; +using Content.Shared.DoAfter; + +namespace Content.Server.Anomaly; + +/// +public sealed class AnomalyScannerSystem : SharedAnomalyScannerSystem +{ + [Dependency] private readonly SecretDataAnomalySystem _secretData = default!; + [Dependency] private readonly AnomalySystem _anomaly = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnScannerAnomalySeverityChanged); + SubscribeLocalEvent(OnScannerAnomalyStabilityChanged); + SubscribeLocalEvent(OnScannerAnomalyHealthChanged); + SubscribeLocalEvent(OnScannerAnomalyBehaviorChanged); + + Subs.BuiEvents( + AnomalyScannerUiKey.Key, + subs => subs.Event(OnScannerUiOpened) + ); + } + + /// Updates device with passed anomaly data. + public void UpdateScannerWithNewAnomaly(EntityUid scanner, EntityUid anomaly, AnomalyScannerComponent? scannerComp = null, AnomalyComponent? anomalyComp = null) + { + if (!Resolve(scanner, ref scannerComp) || !Resolve(anomaly, ref anomalyComp)) + return; + + scannerComp.ScannedAnomaly = anomaly; + UpdateScannerUi(scanner, scannerComp); + + TryComp(scanner, out var appearanceComp); + TryComp(anomaly, out var secretDataComp); + + Appearance.SetData(scanner, AnomalyScannerVisuals.HasAnomaly, true, appearanceComp); + + var stability = _secretData.IsSecret(anomaly, AnomalySecretData.Stability, secretDataComp) + ? AnomalyStabilityVisuals.Stable + : _anomaly.GetStabilityVisualOrStable((anomaly, anomalyComp)); + Appearance.SetData(scanner, AnomalyScannerVisuals.AnomalyStability, stability, appearanceComp); + + var severity = _secretData.IsSecret(anomaly, AnomalySecretData.Severity, secretDataComp) + ? 0 + : anomalyComp.Severity; + Appearance.SetData(scanner, AnomalyScannerVisuals.AnomalySeverity, severity, appearanceComp); + } + + /// Update scanner interface. + public void UpdateScannerUi(EntityUid uid, AnomalyScannerComponent? component = null) + { + if (!Resolve(uid, ref component)) + return; + + TimeSpan? nextPulse = null; + if (TryComp(component.ScannedAnomaly, out var anomalyComponent)) + nextPulse = anomalyComponent.NextPulseTime; + + var state = new AnomalyScannerUserInterfaceState(_anomaly.GetScannerMessage(component), nextPulse); + UI.SetUiState(uid, AnomalyScannerUiKey.Key, state); + } + + /// + public override void Update(float frameTime) + { + base.Update(frameTime); + + var anomalyQuery = EntityQueryEnumerator(); + while (anomalyQuery.MoveNext(out var ent, out var anomaly)) + { + var secondsUntilNextPulse = (anomaly.NextPulseTime - Timing.CurTime).TotalSeconds; + UpdateScannerPulseTimers((ent, anomaly), secondsUntilNextPulse); + } + } + + /// + protected override void OnDoAfter(EntityUid uid, AnomalyScannerComponent component, DoAfterEvent args) + { + if (args.Cancelled || args.Handled || args.Args.Target == null) + return; + + base.OnDoAfter(uid, component, args); + + UpdateScannerWithNewAnomaly(uid, args.Args.Target.Value, component); + } + + private void OnScannerAnomalyHealthChanged(ref AnomalyHealthChangedEvent args) + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var component)) + { + if (component.ScannedAnomaly != args.Anomaly) + continue; + + UpdateScannerUi(uid, component); + } + } + + private void OnScannerUiOpened(EntityUid uid, AnomalyScannerComponent component, BoundUIOpenedEvent args) + { + UpdateScannerUi(uid, component); + } + + private void OnScannerAnomalySeverityChanged(ref AnomalySeverityChangedEvent args) + { + var severity = _secretData.IsSecret(args.Anomaly, AnomalySecretData.Severity) ? 0 : args.Severity; + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var component)) + { + if (component.ScannedAnomaly != args.Anomaly) + continue; + + UpdateScannerUi(uid, component); + Appearance.SetData(uid, AnomalyScannerVisuals.AnomalySeverity, severity); + } + } + + private void OnScannerAnomalyStabilityChanged(ref AnomalyStabilityChangedEvent args) + { + var stability = _secretData.IsSecret(args.Anomaly, AnomalySecretData.Stability) + ? AnomalyStabilityVisuals.Stable + : _anomaly.GetStabilityVisualOrStable(args.Anomaly); + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var component)) + { + if (component.ScannedAnomaly != args.Anomaly) + continue; + + UpdateScannerUi(uid, component); + Appearance.SetData(uid, AnomalyScannerVisuals.AnomalyStability, stability); + } + } + + private void OnScannerAnomalyBehaviorChanged(ref AnomalyBehaviorChangedEvent args) + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var component)) + { + if (component.ScannedAnomaly != args.Anomaly) + continue; + + UpdateScannerUi(uid, component); + // If a field becomes secret, we want to set it to 0 or stable + // If a field becomes visible, we need to set it to the correct value, so we need to get the AnomalyComponent + if (!TryComp(args.Anomaly, out var anomalyComp)) + return; + + TryComp(uid, out var appearanceComp); + TryComp(args.Anomaly, out var secretDataComp); + + var severity = _secretData.IsSecret(args.Anomaly, AnomalySecretData.Severity, secretDataComp) + ? 0 + : anomalyComp.Severity; + Appearance.SetData(uid, AnomalyScannerVisuals.AnomalySeverity, severity, appearanceComp); + + var stability = _secretData.IsSecret(args.Anomaly, AnomalySecretData.Stability, secretDataComp) + ? AnomalyStabilityVisuals.Stable + : _anomaly.GetStabilityVisualOrStable((args.Anomaly, anomalyComp)); + Appearance.SetData(uid, AnomalyScannerVisuals.AnomalyStability, stability, appearanceComp); + } + } + + private void UpdateScannerPulseTimers(Entity anomalyEnt, double secondsUntilNextPulse) + { + if (secondsUntilNextPulse > 5) + return; + + var rounded = Math.Max(0, (int)Math.Ceiling(secondsUntilNextPulse)); + + var scannerQuery = EntityQueryEnumerator(); + while (scannerQuery.MoveNext(out var scannerUid, out var scanner)) + { + if (scanner.ScannedAnomaly != anomalyEnt) + continue; + + Appearance.SetData(scannerUid, AnomalyScannerVisuals.AnomalyNextPulse, rounded); + } + } +} diff --git a/Content.Server/Anomaly/AnomalySystem.Scanner.cs b/Content.Server/Anomaly/AnomalySystem.Scanner.cs deleted file mode 100644 index 9d81878cd8..0000000000 --- a/Content.Server/Anomaly/AnomalySystem.Scanner.cs +++ /dev/null @@ -1,241 +0,0 @@ -using Content.Server.Anomaly.Components; -using Content.Shared.Anomaly; -using Content.Shared.Anomaly.Components; -using Content.Shared.DoAfter; -using Content.Shared.Interaction; -using Robust.Shared.Player; -using Robust.Shared.Utility; - -namespace Content.Server.Anomaly; - -/// -/// This handles the anomaly scanner and it's UI updates. -/// -public sealed partial class AnomalySystem -{ - private void InitializeScanner() - { - SubscribeLocalEvent(OnScannerUiOpened); - SubscribeLocalEvent(OnScannerAfterInteract); - SubscribeLocalEvent(OnDoAfter); - - SubscribeLocalEvent(OnScannerAnomalySeverityChanged); - SubscribeLocalEvent(OnScannerAnomalyHealthChanged); - SubscribeLocalEvent(OnScannerAnomalyBehaviorChanged); - } - - private void OnScannerAnomalyShutdown(ref AnomalyShutdownEvent args) - { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var component)) - { - if (component.ScannedAnomaly != args.Anomaly) - continue; - - _ui.CloseUi(uid, AnomalyScannerUiKey.Key); - } - } - - private void OnScannerAnomalySeverityChanged(ref AnomalySeverityChangedEvent args) - { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var component)) - { - if (component.ScannedAnomaly != args.Anomaly) - continue; - UpdateScannerUi(uid, component); - } - } - - private void OnScannerAnomalyStabilityChanged(ref AnomalyStabilityChangedEvent args) - { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var component)) - { - if (component.ScannedAnomaly != args.Anomaly) - continue; - UpdateScannerUi(uid, component); - } - } - - private void OnScannerAnomalyHealthChanged(ref AnomalyHealthChangedEvent args) - { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var component)) - { - if (component.ScannedAnomaly != args.Anomaly) - continue; - UpdateScannerUi(uid, component); - } - } - - private void OnScannerAnomalyBehaviorChanged(ref AnomalyBehaviorChangedEvent args) - { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var component)) - { - if (component.ScannedAnomaly != args.Anomaly) - continue; - UpdateScannerUi(uid, component); - } - } - - private void OnScannerUiOpened(EntityUid uid, AnomalyScannerComponent component, BoundUIOpenedEvent args) - { - UpdateScannerUi(uid, component); - } - - private void OnScannerAfterInteract(EntityUid uid, AnomalyScannerComponent component, AfterInteractEvent args) - { - if (args.Target is not { } target) - return; - if (!HasComp(target)) - return; - if (!args.CanReach) - return; - - _doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, args.User, component.ScanDoAfterDuration, new ScannerDoAfterEvent(), uid, target: target, used: uid) - { - DistanceThreshold = 2f - }); - } - - private void OnDoAfter(EntityUid uid, AnomalyScannerComponent component, DoAfterEvent args) - { - if (args.Cancelled || args.Handled || args.Args.Target == null) - return; - - Audio.PlayPvs(component.CompleteSound, uid); - Popup.PopupEntity(Loc.GetString("anomaly-scanner-component-scan-complete"), uid); - UpdateScannerWithNewAnomaly(uid, args.Args.Target.Value, component); - - _ui.OpenUi(uid, AnomalyScannerUiKey.Key, args.User); - - args.Handled = true; - } - - public void UpdateScannerUi(EntityUid uid, AnomalyScannerComponent? component = null) - { - if (!Resolve(uid, ref component)) - return; - - TimeSpan? nextPulse = null; - if (TryComp(component.ScannedAnomaly, out var anomalyComponent)) - nextPulse = anomalyComponent.NextPulseTime; - - var state = new AnomalyScannerUserInterfaceState(GetScannerMessage(component), nextPulse); - _ui.SetUiState(uid, AnomalyScannerUiKey.Key, state); - } - - public void UpdateScannerWithNewAnomaly(EntityUid scanner, EntityUid anomaly, AnomalyScannerComponent? scannerComp = null, AnomalyComponent? anomalyComp = null) - { - if (!Resolve(scanner, ref scannerComp) || !Resolve(anomaly, ref anomalyComp)) - return; - - scannerComp.ScannedAnomaly = anomaly; - UpdateScannerUi(scanner, scannerComp); - } - - public FormattedMessage GetScannerMessage(AnomalyScannerComponent component) - { - var msg = new FormattedMessage(); - if (component.ScannedAnomaly is not { } anomaly || !TryComp(anomaly, out var anomalyComp)) - { - msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-no-anomaly")); - return msg; - } - - TryComp(anomaly, out var secret); - - //Severity - if (secret != null && secret.Secret.Contains(AnomalySecretData.Severity)) - msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-severity-percentage-unknown")); - else - msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-severity-percentage", ("percent", anomalyComp.Severity.ToString("P")))); - msg.PushNewline(); - - //Stability - if (secret != null && secret.Secret.Contains(AnomalySecretData.Stability)) - msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-stability-unknown")); - else - { - string stateLoc; - if (anomalyComp.Stability < anomalyComp.DecayThreshold) - stateLoc = Loc.GetString("anomaly-scanner-stability-low"); - else if (anomalyComp.Stability > anomalyComp.GrowthThreshold) - stateLoc = Loc.GetString("anomaly-scanner-stability-high"); - else - stateLoc = Loc.GetString("anomaly-scanner-stability-medium"); - msg.AddMarkupOrThrow(stateLoc); - } - msg.PushNewline(); - - //Point output - if (secret != null && secret.Secret.Contains(AnomalySecretData.OutputPoint)) - msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-point-output-unknown")); - else - msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-point-output", ("point", GetAnomalyPointValue(anomaly, anomalyComp)))); - msg.PushNewline(); - msg.PushNewline(); - - //Particles title - msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-particle-readout")); - msg.PushNewline(); - - //Danger - if (secret != null && secret.Secret.Contains(AnomalySecretData.ParticleDanger)) - msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-particle-danger-unknown")); - else - msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-particle-danger", ("type", GetParticleLocale(anomalyComp.SeverityParticleType)))); - msg.PushNewline(); - - //Unstable - if (secret != null && secret.Secret.Contains(AnomalySecretData.ParticleUnstable)) - msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-particle-unstable-unknown")); - else - msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-particle-unstable", ("type", GetParticleLocale(anomalyComp.DestabilizingParticleType)))); - msg.PushNewline(); - - //Containment - if (secret != null && secret.Secret.Contains(AnomalySecretData.ParticleContainment)) - msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-particle-containment-unknown")); - else - msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-particle-containment", ("type", GetParticleLocale(anomalyComp.WeakeningParticleType)))); - msg.PushNewline(); - - //Transformation - if (secret != null && secret.Secret.Contains(AnomalySecretData.ParticleTransformation)) - msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-particle-transformation-unknown")); - else - msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-particle-transformation", ("type", GetParticleLocale(anomalyComp.TransformationParticleType)))); - - - //Behavior - msg.PushNewline(); - msg.PushNewline(); - msg.AddMarkupOrThrow(Loc.GetString("anomaly-behavior-title")); - msg.PushNewline(); - - if (secret != null && secret.Secret.Contains(AnomalySecretData.Behavior)) - msg.AddMarkupOrThrow(Loc.GetString("anomaly-behavior-unknown")); - else - { - if (anomalyComp.CurrentBehavior != null) - { - var behavior = _prototype.Index(anomalyComp.CurrentBehavior.Value); - - msg.AddMarkupOrThrow("- " + Loc.GetString(behavior.Description)); - msg.PushNewline(); - var mod = Math.Floor((behavior.EarnPointModifier) * 100); - msg.AddMarkupOrThrow("- " + Loc.GetString("anomaly-behavior-point", ("mod", mod))); - } - else - { - msg.AddMarkupOrThrow(Loc.GetString("anomaly-behavior-balanced")); - } - } - - //The timer at the end here is actually added in the ui itself. - return msg; - } -} diff --git a/Content.Server/Anomaly/AnomalySystem.Vessel.cs b/Content.Server/Anomaly/AnomalySystem.Vessel.cs index 98e56a8844..0900f3e96f 100644 --- a/Content.Server/Anomaly/AnomalySystem.Vessel.cs +++ b/Content.Server/Anomaly/AnomalySystem.Vessel.cs @@ -22,20 +22,7 @@ public sealed partial class AnomalySystem SubscribeLocalEvent(OnVesselInteractUsing); SubscribeLocalEvent(OnExamined); SubscribeLocalEvent(OnVesselGetPointsPerSecond); - SubscribeLocalEvent(OnShutdown); - SubscribeLocalEvent(OnStabilityChanged); - } - - private void OnStabilityChanged(ref AnomalyStabilityChangedEvent args) - { - OnVesselAnomalyStabilityChanged(ref args); - OnScannerAnomalyStabilityChanged(ref args); - } - - private void OnShutdown(ref AnomalyShutdownEvent args) - { - OnVesselAnomalyShutdown(ref args); - OnScannerAnomalyShutdown(ref args); + SubscribeLocalEvent(OnVesselAnomalyShutdown); } private void OnExamined(EntityUid uid, AnomalyVesselComponent component, ExaminedEvent args) @@ -141,21 +128,10 @@ public sealed partial class AnomalySystem if (_pointLight.TryGetLight(uid, out var pointLightComponent)) _pointLight.SetEnabled(uid, on, pointLightComponent); - // arbitrary value for the generic visualizer to use. - // i didn't feel like making an enum for this. - var value = 1; - if (TryComp(component.Anomaly, out var anomalyComp)) - { - if (anomalyComp.Stability <= anomalyComp.DecayThreshold) - { - value = 2; - } - else if (anomalyComp.Stability >= anomalyComp.GrowthThreshold) - { - value = 3; - } - } - Appearance.SetData(uid, AnomalyVesselVisuals.AnomalyState, value, appearanceComponent); + if (component.Anomaly == null || !TryGetStabilityVisual(component.Anomaly.Value, out var visual)) + visual = AnomalyStabilityVisuals.Stable; + + Appearance.SetData(uid, AnomalyVesselVisuals.AnomalySeverity, visual, appearanceComponent); _ambient.SetAmbience(uid, on); } diff --git a/Content.Server/Anomaly/AnomalySystem.cs b/Content.Server/Anomaly/AnomalySystem.cs index 9ac0ce7c94..69f18e5eeb 100644 --- a/Content.Server/Anomaly/AnomalySystem.cs +++ b/Content.Server/Anomaly/AnomalySystem.cs @@ -9,7 +9,6 @@ using Content.Server.Station.Systems; using Content.Shared.Anomaly; using Content.Shared.Anomaly.Components; using Content.Shared.Anomaly.Prototypes; -using Content.Shared.DoAfter; using Content.Shared.Random; using Content.Shared.Random.Helpers; using Robust.Server.GameObjects; @@ -18,6 +17,7 @@ using Robust.Shared.Configuration; using Robust.Shared.Physics.Events; using Robust.Shared.Prototypes; using Robust.Shared.Random; +using Robust.Shared.Utility; namespace Content.Server.Anomaly; @@ -30,7 +30,6 @@ public sealed partial class AnomalySystem : SharedAnomalySystem [Dependency] private readonly IPrototypeManager _prototype = default!; [Dependency] private readonly AmbientSoundSystem _ambient = default!; [Dependency] private readonly AtmosphereSystem _atmosphere = default!; - [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; [Dependency] private readonly ExplosionSystem _explosion = default!; [Dependency] private readonly MaterialStorageSystem _material = default!; [Dependency] private readonly SharedPointLightSystem _pointLight = default!; @@ -53,10 +52,9 @@ public sealed partial class AnomalySystem : SharedAnomalySystem SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnShutdown); SubscribeLocalEvent(OnStartCollide); - + SubscribeLocalEvent(OnVesselAnomalyStabilityChanged); InitializeGenerator(); - InitializeScanner(); InitializeVessel(); InitializeCommands(); } @@ -218,4 +216,112 @@ public sealed partial class AnomalySystem : SharedAnomalySystem EntityManager.RemoveComponents(anomaly, behavior.Components); } #endregion + + #region Information + /// + /// Get a formatted message with a summary of all anomaly information for putting on a UI. + /// + public FormattedMessage GetScannerMessage(AnomalyScannerComponent component) + { + var msg = new FormattedMessage(); + if (component.ScannedAnomaly is not { } anomaly || !TryComp(anomaly, out var anomalyComp)) + { + msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-no-anomaly")); + return msg; + } + + TryComp(anomaly, out var secret); + + //Severity + if (secret != null && secret.Secret.Contains(AnomalySecretData.Severity)) + msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-severity-percentage-unknown")); + else + msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-severity-percentage", ("percent", anomalyComp.Severity.ToString("P")))); + msg.PushNewline(); + + //Stability + if (secret != null && secret.Secret.Contains(AnomalySecretData.Stability)) + msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-stability-unknown")); + else + { + string stateLoc; + if (anomalyComp.Stability < anomalyComp.DecayThreshold) + stateLoc = Loc.GetString("anomaly-scanner-stability-low"); + else if (anomalyComp.Stability > anomalyComp.GrowthThreshold) + stateLoc = Loc.GetString("anomaly-scanner-stability-high"); + else + stateLoc = Loc.GetString("anomaly-scanner-stability-medium"); + msg.AddMarkupOrThrow(stateLoc); + } + msg.PushNewline(); + + //Point output + if (secret != null && secret.Secret.Contains(AnomalySecretData.OutputPoint)) + msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-point-output-unknown")); + else + msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-point-output", ("point", GetAnomalyPointValue(anomaly, anomalyComp)))); + msg.PushNewline(); + msg.PushNewline(); + + //Particles title + msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-particle-readout")); + msg.PushNewline(); + + //Danger + if (secret != null && secret.Secret.Contains(AnomalySecretData.ParticleDanger)) + msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-particle-danger-unknown")); + else + msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-particle-danger", ("type", GetParticleLocale(anomalyComp.SeverityParticleType)))); + msg.PushNewline(); + + //Unstable + if (secret != null && secret.Secret.Contains(AnomalySecretData.ParticleUnstable)) + msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-particle-unstable-unknown")); + else + msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-particle-unstable", ("type", GetParticleLocale(anomalyComp.DestabilizingParticleType)))); + msg.PushNewline(); + + //Containment + if (secret != null && secret.Secret.Contains(AnomalySecretData.ParticleContainment)) + msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-particle-containment-unknown")); + else + msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-particle-containment", ("type", GetParticleLocale(anomalyComp.WeakeningParticleType)))); + msg.PushNewline(); + + //Transformation + if (secret != null && secret.Secret.Contains(AnomalySecretData.ParticleTransformation)) + msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-particle-transformation-unknown")); + else + msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-particle-transformation", ("type", GetParticleLocale(anomalyComp.TransformationParticleType)))); + + + //Behavior + msg.PushNewline(); + msg.PushNewline(); + msg.AddMarkupOrThrow(Loc.GetString("anomaly-behavior-title")); + msg.PushNewline(); + + if (secret != null && secret.Secret.Contains(AnomalySecretData.Behavior)) + msg.AddMarkupOrThrow(Loc.GetString("anomaly-behavior-unknown")); + else + { + if (anomalyComp.CurrentBehavior != null) + { + var behavior = _prototype.Index(anomalyComp.CurrentBehavior.Value); + + msg.AddMarkupOrThrow("- " + Loc.GetString(behavior.Description)); + msg.PushNewline(); + var mod = Math.Floor((behavior.EarnPointModifier) * 100); + msg.AddMarkupOrThrow("- " + Loc.GetString("anomaly-behavior-point", ("mod", mod))); + } + else + { + msg.AddMarkupOrThrow(Loc.GetString("anomaly-behavior-balanced")); + } + } + + //The timer at the end here is actually added in the ui itself. + return msg; + } + #endregion } diff --git a/Content.Server/Anomaly/Effects/SecretDataAnomalySystem.cs b/Content.Server/Anomaly/Effects/SecretDataAnomalySystem.cs index cbdc4b04df..0515ed855e 100644 --- a/Content.Server/Anomaly/Effects/SecretDataAnomalySystem.cs +++ b/Content.Server/Anomaly/Effects/SecretDataAnomalySystem.cs @@ -36,5 +36,13 @@ public sealed class SecretDataAnomalySystem : EntitySystem component.Secret.Add(_random.PickAndTake(_deita)); } } + + public bool IsSecret(EntityUid uid, AnomalySecretData item, SecretDataAnomalyComponent? component = null) + { + if (!Resolve(uid, ref component, logMissing: false)) + return false; + + return component.Secret.Contains(item); + } } diff --git a/Content.Server/Entry/IgnoredComponents.cs b/Content.Server/Entry/IgnoredComponents.cs index 58264e14ad..a34842c64f 100644 --- a/Content.Server/Entry/IgnoredComponents.cs +++ b/Content.Server/Entry/IgnoredComponents.cs @@ -20,6 +20,7 @@ namespace Content.Server.Entry "LightFade", "HolidayRsiSwap", "OptionsVisualizer", + "AnomalyScannerScreen", "MultipartMachineGhost" }; } diff --git a/Content.Server/Anomaly/Components/AnomalyScannerComponent.cs b/Content.Shared/Anomaly/Components/AnomalyScannerComponent.cs similarity index 78% rename from Content.Server/Anomaly/Components/AnomalyScannerComponent.cs rename to Content.Shared/Anomaly/Components/AnomalyScannerComponent.cs index 1bc3070494..c49743f630 100644 --- a/Content.Server/Anomaly/Components/AnomalyScannerComponent.cs +++ b/Content.Shared/Anomaly/Components/AnomalyScannerComponent.cs @@ -1,13 +1,15 @@ using Content.Shared.Anomaly; using Robust.Shared.Audio; +using Robust.Shared.GameStates; -namespace Content.Server.Anomaly.Components; +namespace Content.Shared.Anomaly.Components; /// /// This is used for scanning anomalies and /// displaying information about them in the ui /// -[RegisterComponent, Access(typeof(SharedAnomalySystem))] +[RegisterComponent, Access(typeof(SharedAnomalyScannerSystem))] +[NetworkedComponent] public sealed partial class AnomalyScannerComponent : Component { /// @@ -19,12 +21,12 @@ public sealed partial class AnomalyScannerComponent : Component /// /// How long the scan takes /// - [DataField("scanDoAfterDuration")] + [DataField] public float ScanDoAfterDuration = 5; /// /// The sound plays when the scan finished /// - [DataField("completeSound")] + [DataField] public SoundSpecifier? CompleteSound = new SoundPathSpecifier("/Audio/Items/beep.ogg"); } diff --git a/Content.Shared/Anomaly/SharedAnomaly.cs b/Content.Shared/Anomaly/SharedAnomaly.cs index cde61ca336..ac1ba042d4 100644 --- a/Content.Shared/Anomaly/SharedAnomaly.cs +++ b/Content.Shared/Anomaly/SharedAnomaly.cs @@ -17,6 +17,14 @@ public enum AnomalyVisualLayers : byte Animated } +[Serializable, NetSerializable] +public enum AnomalyStabilityVisuals : byte +{ + Stable = 1, + Decaying = 2, + Growing = 3, +} + /// /// The types of anomalous particles used /// for interfacing with anomalies. @@ -41,7 +49,7 @@ public enum AnomalousParticleType : byte public enum AnomalyVesselVisuals : byte { HasAnomaly, - AnomalyState + AnomalySeverity, } [Serializable, NetSerializable] @@ -68,6 +76,27 @@ public enum AnomalyScannerUiKey : byte Key } +[Serializable, NetSerializable] +public enum AnomalyScannerVisuals : byte +{ + HasAnomaly, + AnomalyStability, + AnomalySeverity, + AnomalyNextPulse, + AnomalyIsSupercritical, +} + +[Serializable, NetSerializable] +public enum AnomalyScannerVisualLayers : byte +{ + Base, + Screen, + SeverityMask, + Stability, + Pulse, + Supercritical, +} + [Serializable, NetSerializable] public sealed class AnomalyScannerUserInterfaceState : BoundUserInterfaceState { diff --git a/Content.Shared/Anomaly/SharedAnomalyScannerSystem.cs b/Content.Shared/Anomaly/SharedAnomalyScannerSystem.cs new file mode 100644 index 0000000000..42d57c65f0 --- /dev/null +++ b/Content.Shared/Anomaly/SharedAnomalyScannerSystem.cs @@ -0,0 +1,86 @@ +using Content.Shared.Anomaly.Components; +using Content.Shared.DoAfter; +using Content.Shared.Interaction; +using Content.Shared.Popups; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Timing; + +namespace Content.Shared.Anomaly; + +/// System for controlling anomaly scanner device. +public abstract class SharedAnomalyScannerSystem : EntitySystem +{ + [Dependency] protected readonly SharedPopupSystem Popup = default!; + [Dependency] protected readonly SharedAudioSystem Audio = default!; + [Dependency] protected readonly IGameTiming Timing = default!; + [Dependency] protected readonly SharedAppearanceSystem Appearance = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; + [Dependency] protected readonly SharedUserInterfaceSystem UI = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnDoAfter); + SubscribeLocalEvent(OnScannerAfterInteract); + SubscribeLocalEvent(OnScannerAnomalyShutdown); + } + + private void OnScannerAnomalyShutdown(ref AnomalyShutdownEvent args) + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var component)) + { + if (component.ScannedAnomaly != args.Anomaly) + continue; + + UI.CloseUi(uid, AnomalyScannerUiKey.Key); + // Anomaly over, reset all the appearance data + Appearance.SetData(uid, AnomalyScannerVisuals.HasAnomaly, false); + Appearance.SetData(uid, AnomalyScannerVisuals.AnomalyIsSupercritical, false); + Appearance.SetData(uid, AnomalyScannerVisuals.AnomalyNextPulse, 0); + Appearance.SetData(uid, AnomalyScannerVisuals.AnomalySeverity, 0); + Appearance.SetData(uid, AnomalyScannerVisuals.AnomalyStability, AnomalyStabilityVisuals.Stable); + } + } + + private void OnScannerAfterInteract(EntityUid uid, AnomalyScannerComponent component, AfterInteractEvent args) + { + if (args.Target is not { } target) + return; + + if (!HasComp(target)) + return; + + if (!args.CanReach) + return; + + var doAfterArgs = new DoAfterArgs( + EntityManager, + args.User, + component.ScanDoAfterDuration, + new ScannerDoAfterEvent(), + uid, + target: target, + used: uid + ) + { + DistanceThreshold = 2f + }; + _doAfter.TryStartDoAfter(doAfterArgs); + } + + protected virtual void OnDoAfter(EntityUid uid, AnomalyScannerComponent component, DoAfterEvent args) + { + if (args.Cancelled || args.Handled || args.Args.Target == null) + return; + + Audio.PlayPredicted(component.CompleteSound, uid, args.User); + Popup.PopupPredicted(Loc.GetString("anomaly-scanner-component-scan-complete"), uid, args.User); + + UI.OpenUi(uid, AnomalyScannerUiKey.Key, args.User); + + args.Handled = true; + } + +} diff --git a/Content.Shared/Anomaly/SharedAnomalySystem.cs b/Content.Shared/Anomaly/SharedAnomalySystem.cs index ee3903a1d9..452dc73e26 100644 --- a/Content.Shared/Anomaly/SharedAnomalySystem.cs +++ b/Content.Shared/Anomaly/SharedAnomalySystem.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using Content.Shared.Administration.Logs; using Content.Shared.Anomaly.Components; using Content.Shared.Anomaly.Prototypes; @@ -140,6 +141,7 @@ public abstract class SharedAnomalySystem : EntitySystem var super = AddComp(ent); super.EndTime = Timing.CurTime + ent.Comp.SupercriticalDuration; Appearance.SetData(ent, AnomalyVisuals.Supercritical, true); + SetScannerSupercritical((ent, ent.Comp), true); Dirty(ent, super); } @@ -340,7 +342,8 @@ public abstract class SharedAnomalySystem : EntitySystem ChangeAnomalyHealth(ent, anomaly.HealthChangePerSecond * frameTime, anomaly); } - if (Timing.CurTime > anomaly.NextPulseTime) + var secondsUntilNextPulse = (anomaly.NextPulseTime - Timing.CurTime).TotalSeconds; + if (secondsUntilNextPulse < 0) { DoAnomalyPulse(ent, anomaly); } @@ -366,6 +369,18 @@ public abstract class SharedAnomalySystem : EntitySystem } } + private void SetScannerSupercritical(Entity anomalyEnt, bool value) + { + var scannerQuery = EntityQueryEnumerator(); + while (scannerQuery.MoveNext(out var scannerUid, out var scanner)) + { + if (scanner.ScannedAnomaly != anomalyEnt) + continue; + + Appearance.SetData(scannerUid, AnomalyScannerVisuals.AnomalyIsSupercritical, value); + } + } + /// /// Gets random points around the anomaly based on the given parameters. /// @@ -441,6 +456,33 @@ public abstract class SharedAnomalySystem : EntitySystem } return resultList; } + + public bool TryGetStabilityVisual(Entity ent, [NotNullWhen(true)] out AnomalyStabilityVisuals? visual) + { + visual = null; + if (!Resolve(ent, ref ent.Comp, logMissing: false)) + return false; + + visual = AnomalyStabilityVisuals.Stable; + if (ent.Comp.Stability <= ent.Comp.DecayThreshold) + { + visual = AnomalyStabilityVisuals.Decaying; + } + else if (ent.Comp.Stability >= ent.Comp.GrowthThreshold) + { + visual = AnomalyStabilityVisuals.Growing; + } + + return true; + } + + public AnomalyStabilityVisuals GetStabilityVisualOrStable(Entity ent) + { + if(TryGetStabilityVisual(ent, out var visual)) + return visual.Value; + + return AnomalyStabilityVisuals.Stable; + } } [DataRecord] diff --git a/Resources/Prototypes/Entities/Objects/Specific/Research/anomaly.yml b/Resources/Prototypes/Entities/Objects/Specific/Research/anomaly.yml index f3a82abd72..71dbed0aac 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Research/anomaly.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Research/anomaly.yml @@ -6,7 +6,26 @@ components: - type: Sprite sprite: Objects/Specific/Research/anomalyscanner.rsi - state: icon + layers: + - state: icon + map: ["enum.AnomalyScannerVisualLayers.Base"] + - map: ["enum.AnomalyScannerVisualLayers.Screen"] + visible: false + shader: unshaded + - state: severity_mask + map: ["enum.AnomalyScannerVisualLayers.SeverityMask"] + visible: false + shader: unshaded + - map: ["enum.AnomalyScannerVisualLayers.Stability"] + visible: false + shader: unshaded + - visible: false + map: ["enum.AnomalyScannerVisualLayers.Pulse"] + shader: unshaded + - state: supercritical + map: ["enum.AnomalyScannerVisualLayers.Supercritical"] + shader: unshaded + visible: false - type: ActivatableUI key: enum.AnomalyScannerUiKey.Key requireActiveHand: false @@ -15,7 +34,35 @@ interfaces: enum.AnomalyScannerUiKey.Key: type: AnomalyScannerBoundUserInterface + - type: Appearance + - type: GenericVisualizer + visuals: + enum.AnomalyScannerVisuals.HasAnomaly: + enum.AnomalyScannerVisualLayers.Screen: + True: { visible: true } + False: { visible: false } + enum.AnomalyScannerVisualLayers.SeverityMask: + True: { visible: true } + False: { visible: false } + enum.AnomalyScannerVisuals.AnomalyStability: + enum.AnomalyScannerVisualLayers.Stability: + Stable: { visible: false } + Decaying: { visible: true, state: decaying } + Growing: { visible: true, state: growing } + enum.AnomalyScannerVisuals.AnomalyNextPulse: + enum.AnomalyScannerVisualLayers.Pulse: + 0: { visible: false } + 1: { visible: true, state: timer_1 } + 2: { visible: true, state: timer_2 } + 3: { visible: true, state: timer_3 } + 4: { visible: true, state: timer_4 } + 5: { visible: true, state: timer_5 } + enum.AnomalyScannerVisuals.AnomalyIsSupercritical: + enum.AnomalyScannerVisualLayers.Supercritical: + True: { visible: true } + False: { visible: false } - type: AnomalyScanner + - type: AnomalyScannerScreen - type: GuideHelp guides: - ScannersAndVessels diff --git a/Resources/Prototypes/Entities/Structures/Machines/anomaly_equipment.yml b/Resources/Prototypes/Entities/Structures/Machines/anomaly_equipment.yml index 6ef8f7262f..064dc68c68 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/anomaly_equipment.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/anomaly_equipment.yml @@ -53,15 +53,15 @@ enum.AnomalyVesselVisualLayers.Base: True: { visible: true } False: { visible: false } - enum.AnomalyVesselVisuals.AnomalyState: + enum.AnomalyVesselVisuals.AnomalySeverity: enum.PowerDeviceVisualLayers.Powered: - 1: { state: powered-1 } - 2: { state: powered-2 } - 3: { state: powered-3 } + Stable: { state: powered-1 } + Decaying: { state: powered-2 } + Growing: { state: powered-3 } enum.AnomalyVesselVisualLayers.Base: - 1: { state: anomaly-1 } - 2: { state: anomaly-2 } - 3: { state: anomaly-3 } + Stable: { state: anomaly-1 } + Decaying: { state: anomaly-2 } + Growing: { state: anomaly-3 } enum.WiresVisuals.MaintenancePanelState: enum.WiresVisualLayers.MaintenancePanel: True: { visible: false } diff --git a/Resources/Textures/Objects/Specific/Research/anomalyscanner.rsi/decaying.png b/Resources/Textures/Objects/Specific/Research/anomalyscanner.rsi/decaying.png new file mode 100644 index 0000000000..7335e13cb0 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Research/anomalyscanner.rsi/decaying.png differ diff --git a/Resources/Textures/Objects/Specific/Research/anomalyscanner.rsi/growing.png b/Resources/Textures/Objects/Specific/Research/anomalyscanner.rsi/growing.png new file mode 100644 index 0000000000..3c9eeba747 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Research/anomalyscanner.rsi/growing.png differ diff --git a/Resources/Textures/Objects/Specific/Research/anomalyscanner.rsi/meta.json b/Resources/Textures/Objects/Specific/Research/anomalyscanner.rsi/meta.json index 289c6bb269..f0a877bc87 100644 --- a/Resources/Textures/Objects/Specific/Research/anomalyscanner.rsi/meta.json +++ b/Resources/Textures/Objects/Specific/Research/anomalyscanner.rsi/meta.json @@ -17,6 +17,45 @@ { "name": "inhand-right", "directions": 4 + }, + { + "name": "growing", + "delays": [ + [ 0.2, 0.2, 0.2 ] + ] + }, + { + "name": "decaying", + "delays": [ + [ 0.2, 0.2, 0.2 ] + ] + }, + { + "name": "severity_mask", + "delays": [ + [ 0.25, 0.25, 0.25, 0.25 ] + ] + }, + { + "name": "timer_1" + }, + { + "name": "timer_2" + }, + { + "name": "timer_3" + }, + { + "name": "timer_4" + }, + { + "name": "timer_5" + }, + { + "name": "supercritical", + "delays": [ + [ 0.125, 0.125, 0.125, 0.125 ] + ] } ] } diff --git a/Resources/Textures/Objects/Specific/Research/anomalyscanner.rsi/severity_mask.png b/Resources/Textures/Objects/Specific/Research/anomalyscanner.rsi/severity_mask.png new file mode 100644 index 0000000000..4d0ae9a3ae Binary files /dev/null and b/Resources/Textures/Objects/Specific/Research/anomalyscanner.rsi/severity_mask.png differ diff --git a/Resources/Textures/Objects/Specific/Research/anomalyscanner.rsi/supercritical.png b/Resources/Textures/Objects/Specific/Research/anomalyscanner.rsi/supercritical.png new file mode 100644 index 0000000000..fedb3ba03b Binary files /dev/null and b/Resources/Textures/Objects/Specific/Research/anomalyscanner.rsi/supercritical.png differ diff --git a/Resources/Textures/Objects/Specific/Research/anomalyscanner.rsi/timer_1.png b/Resources/Textures/Objects/Specific/Research/anomalyscanner.rsi/timer_1.png new file mode 100644 index 0000000000..47b483bf5d Binary files /dev/null and b/Resources/Textures/Objects/Specific/Research/anomalyscanner.rsi/timer_1.png differ diff --git a/Resources/Textures/Objects/Specific/Research/anomalyscanner.rsi/timer_2.png b/Resources/Textures/Objects/Specific/Research/anomalyscanner.rsi/timer_2.png new file mode 100644 index 0000000000..0a13874777 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Research/anomalyscanner.rsi/timer_2.png differ diff --git a/Resources/Textures/Objects/Specific/Research/anomalyscanner.rsi/timer_3.png b/Resources/Textures/Objects/Specific/Research/anomalyscanner.rsi/timer_3.png new file mode 100644 index 0000000000..fd1ebf7da4 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Research/anomalyscanner.rsi/timer_3.png differ diff --git a/Resources/Textures/Objects/Specific/Research/anomalyscanner.rsi/timer_4.png b/Resources/Textures/Objects/Specific/Research/anomalyscanner.rsi/timer_4.png new file mode 100644 index 0000000000..e3c79e9ab2 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Research/anomalyscanner.rsi/timer_4.png differ diff --git a/Resources/Textures/Objects/Specific/Research/anomalyscanner.rsi/timer_5.png b/Resources/Textures/Objects/Specific/Research/anomalyscanner.rsi/timer_5.png new file mode 100644 index 0000000000..943f391907 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Research/anomalyscanner.rsi/timer_5.png differ