--- /dev/null
+using Robust.Client.Graphics;
+using SixLabors.ImageSharp.PixelFormats;
+
+namespace Content.Client.Anomaly;
+
+/// <summary>
+/// This component creates and handles the drawing of a ScreenTexture to be used on the Anomaly Scanner
+/// for an indicator of Anomaly Severity.
+/// </summary>
+/// <remarks>
+/// 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.
+/// </remarks>
+[RegisterComponent]
+[Access(typeof(AnomalyScannerSystem))]
+public sealed partial class AnomalyScannerScreenComponent : Component
+{
+ /// <summary>
+ /// This is the texture drawn as a layer on the Anomaly Scanner device.
+ /// </summary>
+ public OwnedTexture? ScreenTexture;
+
+ /// <summary>
+ /// A small buffer that we can reuse to draw the severity bar.
+ /// </summary>
+ public Rgba32[]? BarBuf;
+
+ /// <summary>
+ /// The position of the top-left of the severity bar in pixels.
+ /// </summary>
+ [DataField(readOnly: true)]
+ public Vector2i Offset = new Vector2i(12, 17);
+
+ /// <summary>
+ /// The width and height of the severity bar in pixels.
+ /// </summary>
+ [DataField(readOnly: true)]
+ public Vector2i Size = new Vector2i(10, 3);
+}
--- /dev/null
+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;
+
+/// <inheritdoc cref="SharedAnomalyScannerSystem"/>
+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<AnomalyScannerScreenComponent, ComponentInit>(OnComponentInit);
+ SubscribeLocalEvent<AnomalyScannerScreenComponent, ComponentStartup>(OnComponentStartup);
+ SubscribeLocalEvent<AnomalyScannerScreenComponent, AppearanceChangeEvent>(OnScannerAppearanceChanged);
+ }
+
+ private void OnComponentInit(Entity<AnomalyScannerScreenComponent> ent, ref ComponentInit args)
+ {
+ if(!_sprite.TryGetLayer(ent.Owner, AnomalyScannerVisualLayers.Base, out var layer, true))
+ return;
+
+ // Allocate the OwnedTexture
+ ent.Comp.ScreenTexture = _clyde.CreateBlankTexture<Rgba32>(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<Rgba32>(EmptyTexture));
+
+ // Initialize bar drawing buffer
+ ent.Comp.BarBuf = new Rgba32[ent.Comp.Size.X * ent.Comp.Size.Y];
+ }
+
+ private void OnComponentStartup(Entity<AnomalyScannerScreenComponent> ent, ref ComponentStartup args)
+ {
+ if (!TryComp<SpriteComponent>(ent, out var sprite))
+ return;
+
+ _sprite.LayerSetTexture((ent, sprite), AnomalyScannerVisualLayers.Screen, ent.Comp.ScreenTexture);
+ }
+
+ private void OnScannerAppearanceChanged(Entity<AnomalyScannerScreenComponent> 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<Rgba32>(ent.Comp.BarBuf)
+ );
+ }
+ catch (IndexOutOfRangeException)
+ {
+ Log.Warning($"Bar dimensions out of bounds with the texture on entity {ent.Owner}");
+ }
+ }
+}
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!;
SubscribeLocalEvent<AnomalySupercriticalComponent, ComponentShutdown>(OnShutdown);
}
+
private void OnStartup(EntityUid uid, AnomalyComponent component, ComponentStartup args)
{
_floating.FloatAnimation(uid, component.FloatingOffset, component.AnimationKey, component.AnimationTime);
--- /dev/null
+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;
+
+/// <inheritdoc cref="SharedAnomalyScannerSystem"/>
+public sealed class AnomalyScannerSystem : SharedAnomalyScannerSystem
+{
+ [Dependency] private readonly SecretDataAnomalySystem _secretData = default!;
+ [Dependency] private readonly AnomalySystem _anomaly = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent<AnomalySeverityChangedEvent>(OnScannerAnomalySeverityChanged);
+ SubscribeLocalEvent<AnomalyStabilityChangedEvent>(OnScannerAnomalyStabilityChanged);
+ SubscribeLocalEvent<AnomalyHealthChangedEvent>(OnScannerAnomalyHealthChanged);
+ SubscribeLocalEvent<AnomalyBehaviorChangedEvent>(OnScannerAnomalyBehaviorChanged);
+
+ Subs.BuiEvents<AnomalyScannerComponent>(
+ AnomalyScannerUiKey.Key,
+ subs => subs.Event<BoundUIOpenedEvent>(OnScannerUiOpened)
+ );
+ }
+
+ /// <summary> Updates device with passed anomaly data. </summary>
+ 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<AppearanceComponent>(scanner, out var appearanceComp);
+ TryComp<SecretDataAnomalyComponent>(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);
+ }
+
+ /// <summary> Update scanner interface. </summary>
+ public void UpdateScannerUi(EntityUid uid, AnomalyScannerComponent? component = null)
+ {
+ if (!Resolve(uid, ref component))
+ return;
+
+ TimeSpan? nextPulse = null;
+ if (TryComp<AnomalyComponent>(component.ScannedAnomaly, out var anomalyComponent))
+ nextPulse = anomalyComponent.NextPulseTime;
+
+ var state = new AnomalyScannerUserInterfaceState(_anomaly.GetScannerMessage(component), nextPulse);
+ UI.SetUiState(uid, AnomalyScannerUiKey.Key, state);
+ }
+
+ /// <inheritdoc />
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ var anomalyQuery = EntityQueryEnumerator<AnomalyComponent>();
+ while (anomalyQuery.MoveNext(out var ent, out var anomaly))
+ {
+ var secondsUntilNextPulse = (anomaly.NextPulseTime - Timing.CurTime).TotalSeconds;
+ UpdateScannerPulseTimers((ent, anomaly), secondsUntilNextPulse);
+ }
+ }
+
+ /// <inheritdoc />
+ 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<AnomalyScannerComponent>();
+ 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<AnomalyScannerComponent>();
+ 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<AnomalyScannerComponent>();
+ 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<AnomalyScannerComponent>();
+ 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<AnomalyComponent>(args.Anomaly, out var anomalyComp))
+ return;
+
+ TryComp<AppearanceComponent>(uid, out var appearanceComp);
+ TryComp<SecretDataAnomalyComponent>(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<AnomalyComponent> anomalyEnt, double secondsUntilNextPulse)
+ {
+ if (secondsUntilNextPulse > 5)
+ return;
+
+ var rounded = Math.Max(0, (int)Math.Ceiling(secondsUntilNextPulse));
+
+ var scannerQuery = EntityQueryEnumerator<AnomalyScannerComponent>();
+ while (scannerQuery.MoveNext(out var scannerUid, out var scanner))
+ {
+ if (scanner.ScannedAnomaly != anomalyEnt)
+ continue;
+
+ Appearance.SetData(scannerUid, AnomalyScannerVisuals.AnomalyNextPulse, rounded);
+ }
+ }
+}
+++ /dev/null
-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;
-
-/// <summary>
-/// This handles the anomaly scanner and it's UI updates.
-/// </summary>
-public sealed partial class AnomalySystem
-{
- private void InitializeScanner()
- {
- SubscribeLocalEvent<AnomalyScannerComponent, BoundUIOpenedEvent>(OnScannerUiOpened);
- SubscribeLocalEvent<AnomalyScannerComponent, AfterInteractEvent>(OnScannerAfterInteract);
- SubscribeLocalEvent<AnomalyScannerComponent, ScannerDoAfterEvent>(OnDoAfter);
-
- SubscribeLocalEvent<AnomalySeverityChangedEvent>(OnScannerAnomalySeverityChanged);
- SubscribeLocalEvent<AnomalyHealthChangedEvent>(OnScannerAnomalyHealthChanged);
- SubscribeLocalEvent<AnomalyBehaviorChangedEvent>(OnScannerAnomalyBehaviorChanged);
- }
-
- private void OnScannerAnomalyShutdown(ref AnomalyShutdownEvent args)
- {
- var query = EntityQueryEnumerator<AnomalyScannerComponent>();
- 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<AnomalyScannerComponent>();
- 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<AnomalyScannerComponent>();
- 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<AnomalyScannerComponent>();
- 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<AnomalyScannerComponent>();
- 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<AnomalyComponent>(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<AnomalyComponent>(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<AnomalyComponent>(anomaly, out var anomalyComp))
- {
- msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-no-anomaly"));
- return msg;
- }
-
- TryComp<SecretDataAnomalyComponent>(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;
- }
-}
SubscribeLocalEvent<AnomalyVesselComponent, InteractUsingEvent>(OnVesselInteractUsing);
SubscribeLocalEvent<AnomalyVesselComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<AnomalyVesselComponent, ResearchServerGetPointsPerSecondEvent>(OnVesselGetPointsPerSecond);
- SubscribeLocalEvent<AnomalyShutdownEvent>(OnShutdown);
- SubscribeLocalEvent<AnomalyStabilityChangedEvent>(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<AnomalyShutdownEvent>(OnVesselAnomalyShutdown);
}
private void OnExamined(EntityUid uid, AnomalyVesselComponent component, ExaminedEvent args)
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<AnomalyComponent>(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);
}
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;
using Robust.Shared.Physics.Events;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
+using Robust.Shared.Utility;
namespace Content.Server.Anomaly;
[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!;
SubscribeLocalEvent<AnomalyComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<AnomalyComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<AnomalyComponent, StartCollideEvent>(OnStartCollide);
-
+ SubscribeLocalEvent<AnomalyStabilityChangedEvent>(OnVesselAnomalyStabilityChanged);
InitializeGenerator();
- InitializeScanner();
InitializeVessel();
InitializeCommands();
}
EntityManager.RemoveComponents(anomaly, behavior.Components);
}
#endregion
+
+ #region Information
+ /// <summary>
+ /// Get a formatted message with a summary of all anomaly information for putting on a UI.
+ /// </summary>
+ public FormattedMessage GetScannerMessage(AnomalyScannerComponent component)
+ {
+ var msg = new FormattedMessage();
+ if (component.ScannedAnomaly is not { } anomaly || !TryComp<AnomalyComponent>(anomaly, out var anomalyComp))
+ {
+ msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-no-anomaly"));
+ return msg;
+ }
+
+ TryComp<SecretDataAnomalyComponent>(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
}
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);
+ }
}
"LightFade",
"HolidayRsiSwap",
"OptionsVisualizer",
+ "AnomalyScannerScreen",
"MultipartMachineGhost"
};
}
using Content.Shared.Anomaly;
using Robust.Shared.Audio;
+using Robust.Shared.GameStates;
-namespace Content.Server.Anomaly.Components;
+namespace Content.Shared.Anomaly.Components;
/// <summary>
/// This is used for scanning anomalies and
/// displaying information about them in the ui
/// </summary>
-[RegisterComponent, Access(typeof(SharedAnomalySystem))]
+[RegisterComponent, Access(typeof(SharedAnomalyScannerSystem))]
+[NetworkedComponent]
public sealed partial class AnomalyScannerComponent : Component
{
/// <summary>
/// <summary>
/// How long the scan takes
/// </summary>
- [DataField("scanDoAfterDuration")]
+ [DataField]
public float ScanDoAfterDuration = 5;
/// <summary>
/// The sound plays when the scan finished
/// </summary>
- [DataField("completeSound")]
+ [DataField]
public SoundSpecifier? CompleteSound = new SoundPathSpecifier("/Audio/Items/beep.ogg");
}
Animated
}
+[Serializable, NetSerializable]
+public enum AnomalyStabilityVisuals : byte
+{
+ Stable = 1,
+ Decaying = 2,
+ Growing = 3,
+}
+
/// <summary>
/// The types of anomalous particles used
/// for interfacing with anomalies.
public enum AnomalyVesselVisuals : byte
{
HasAnomaly,
- AnomalyState
+ AnomalySeverity,
}
[Serializable, NetSerializable]
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
{
--- /dev/null
+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;
+
+/// <summary> System for controlling anomaly scanner device. </summary>
+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<AnomalyScannerComponent, ScannerDoAfterEvent>(OnDoAfter);
+ SubscribeLocalEvent<AnomalyScannerComponent, AfterInteractEvent>(OnScannerAfterInteract);
+ SubscribeLocalEvent<AnomalyShutdownEvent>(OnScannerAnomalyShutdown);
+ }
+
+ private void OnScannerAnomalyShutdown(ref AnomalyShutdownEvent args)
+ {
+ var query = EntityQueryEnumerator<AnomalyScannerComponent>();
+ 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<AnomalyComponent>(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;
+ }
+
+}
+using System.Diagnostics.CodeAnalysis;
using Content.Shared.Administration.Logs;
using Content.Shared.Anomaly.Components;
using Content.Shared.Anomaly.Prototypes;
var super = AddComp<AnomalySupercriticalComponent>(ent);
super.EndTime = Timing.CurTime + ent.Comp.SupercriticalDuration;
Appearance.SetData(ent, AnomalyVisuals.Supercritical, true);
+ SetScannerSupercritical((ent, ent.Comp), true);
Dirty(ent, super);
}
ChangeAnomalyHealth(ent, anomaly.HealthChangePerSecond * frameTime, anomaly);
}
- if (Timing.CurTime > anomaly.NextPulseTime)
+ var secondsUntilNextPulse = (anomaly.NextPulseTime - Timing.CurTime).TotalSeconds;
+ if (secondsUntilNextPulse < 0)
{
DoAnomalyPulse(ent, anomaly);
}
}
}
+ private void SetScannerSupercritical(Entity<AnomalyComponent> anomalyEnt, bool value)
+ {
+ var scannerQuery = EntityQueryEnumerator<AnomalyScannerComponent>();
+ while (scannerQuery.MoveNext(out var scannerUid, out var scanner))
+ {
+ if (scanner.ScannedAnomaly != anomalyEnt)
+ continue;
+
+ Appearance.SetData(scannerUid, AnomalyScannerVisuals.AnomalyIsSupercritical, value);
+ }
+ }
+
/// <summary>
/// Gets random points around the anomaly based on the given parameters.
/// </summary>
}
return resultList;
}
+
+ public bool TryGetStabilityVisual(Entity<AnomalyComponent?> 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<AnomalyComponent?> ent)
+ {
+ if(TryGetStabilityVisual(ent, out var visual))
+ return visual.Value;
+
+ return AnomalyStabilityVisuals.Stable;
+ }
}
[DataRecord]
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
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
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 }
{
"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 ]
+ ]
}
]
}