Dynamic anomaly scanner texture (#37585)
40
Content.Client/Anomaly/AnomalyScannerScreenComponent.cs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
110
Content.Client/Anomaly/AnomalyScannerSystem.cs
Normal file
@@ -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;
|
||||||
|
|
||||||
|
/// <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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ using Robust.Shared.Timing;
|
|||||||
|
|
||||||
namespace Content.Client.Anomaly;
|
namespace Content.Client.Anomaly;
|
||||||
|
|
||||||
public sealed class AnomalySystem : SharedAnomalySystem
|
public sealed partial class AnomalySystem : SharedAnomalySystem
|
||||||
{
|
{
|
||||||
[Dependency] private readonly IGameTiming _timing = default!;
|
[Dependency] private readonly IGameTiming _timing = default!;
|
||||||
[Dependency] private readonly FloatingVisualizerSystem _floating = default!;
|
[Dependency] private readonly FloatingVisualizerSystem _floating = default!;
|
||||||
@@ -24,6 +24,7 @@ public sealed class AnomalySystem : SharedAnomalySystem
|
|||||||
|
|
||||||
SubscribeLocalEvent<AnomalySupercriticalComponent, ComponentShutdown>(OnShutdown);
|
SubscribeLocalEvent<AnomalySupercriticalComponent, ComponentShutdown>(OnShutdown);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnStartup(EntityUid uid, AnomalyComponent component, ComponentStartup args)
|
private void OnStartup(EntityUid uid, AnomalyComponent component, ComponentStartup args)
|
||||||
{
|
{
|
||||||
_floating.FloatAnimation(uid, component.FloatingOffset, component.AnimationKey, component.AnimationTime);
|
_floating.FloatAnimation(uid, component.FloatingOffset, component.AnimationKey, component.AnimationTime);
|
||||||
|
|||||||
185
Content.Server/Anomaly/AnomalyScannerSystem.cs
Normal file
@@ -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;
|
||||||
|
|
||||||
|
/// <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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
|
|
||||||
/// <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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -22,20 +22,7 @@ public sealed partial class AnomalySystem
|
|||||||
SubscribeLocalEvent<AnomalyVesselComponent, InteractUsingEvent>(OnVesselInteractUsing);
|
SubscribeLocalEvent<AnomalyVesselComponent, InteractUsingEvent>(OnVesselInteractUsing);
|
||||||
SubscribeLocalEvent<AnomalyVesselComponent, ExaminedEvent>(OnExamined);
|
SubscribeLocalEvent<AnomalyVesselComponent, ExaminedEvent>(OnExamined);
|
||||||
SubscribeLocalEvent<AnomalyVesselComponent, ResearchServerGetPointsPerSecondEvent>(OnVesselGetPointsPerSecond);
|
SubscribeLocalEvent<AnomalyVesselComponent, ResearchServerGetPointsPerSecondEvent>(OnVesselGetPointsPerSecond);
|
||||||
SubscribeLocalEvent<AnomalyShutdownEvent>(OnShutdown);
|
SubscribeLocalEvent<AnomalyShutdownEvent>(OnVesselAnomalyShutdown);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnExamined(EntityUid uid, AnomalyVesselComponent component, ExaminedEvent args)
|
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))
|
if (_pointLight.TryGetLight(uid, out var pointLightComponent))
|
||||||
_pointLight.SetEnabled(uid, on, pointLightComponent);
|
_pointLight.SetEnabled(uid, on, pointLightComponent);
|
||||||
|
|
||||||
// arbitrary value for the generic visualizer to use.
|
if (component.Anomaly == null || !TryGetStabilityVisual(component.Anomaly.Value, out var visual))
|
||||||
// i didn't feel like making an enum for this.
|
visual = AnomalyStabilityVisuals.Stable;
|
||||||
var value = 1;
|
|
||||||
if (TryComp<AnomalyComponent>(component.Anomaly, out var anomalyComp))
|
Appearance.SetData(uid, AnomalyVesselVisuals.AnomalySeverity, visual, appearanceComponent);
|
||||||
{
|
|
||||||
if (anomalyComp.Stability <= anomalyComp.DecayThreshold)
|
|
||||||
{
|
|
||||||
value = 2;
|
|
||||||
}
|
|
||||||
else if (anomalyComp.Stability >= anomalyComp.GrowthThreshold)
|
|
||||||
{
|
|
||||||
value = 3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Appearance.SetData(uid, AnomalyVesselVisuals.AnomalyState, value, appearanceComponent);
|
|
||||||
|
|
||||||
_ambient.SetAmbience(uid, on);
|
_ambient.SetAmbience(uid, on);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ using Content.Server.Station.Systems;
|
|||||||
using Content.Shared.Anomaly;
|
using Content.Shared.Anomaly;
|
||||||
using Content.Shared.Anomaly.Components;
|
using Content.Shared.Anomaly.Components;
|
||||||
using Content.Shared.Anomaly.Prototypes;
|
using Content.Shared.Anomaly.Prototypes;
|
||||||
using Content.Shared.DoAfter;
|
|
||||||
using Content.Shared.Random;
|
using Content.Shared.Random;
|
||||||
using Content.Shared.Random.Helpers;
|
using Content.Shared.Random.Helpers;
|
||||||
using Robust.Server.GameObjects;
|
using Robust.Server.GameObjects;
|
||||||
@@ -18,6 +17,7 @@ using Robust.Shared.Configuration;
|
|||||||
using Robust.Shared.Physics.Events;
|
using Robust.Shared.Physics.Events;
|
||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
using Robust.Shared.Random;
|
using Robust.Shared.Random;
|
||||||
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
namespace Content.Server.Anomaly;
|
namespace Content.Server.Anomaly;
|
||||||
|
|
||||||
@@ -30,7 +30,6 @@ public sealed partial class AnomalySystem : SharedAnomalySystem
|
|||||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||||
[Dependency] private readonly AmbientSoundSystem _ambient = default!;
|
[Dependency] private readonly AmbientSoundSystem _ambient = default!;
|
||||||
[Dependency] private readonly AtmosphereSystem _atmosphere = default!;
|
[Dependency] private readonly AtmosphereSystem _atmosphere = default!;
|
||||||
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
|
|
||||||
[Dependency] private readonly ExplosionSystem _explosion = default!;
|
[Dependency] private readonly ExplosionSystem _explosion = default!;
|
||||||
[Dependency] private readonly MaterialStorageSystem _material = default!;
|
[Dependency] private readonly MaterialStorageSystem _material = default!;
|
||||||
[Dependency] private readonly SharedPointLightSystem _pointLight = default!;
|
[Dependency] private readonly SharedPointLightSystem _pointLight = default!;
|
||||||
@@ -53,10 +52,9 @@ public sealed partial class AnomalySystem : SharedAnomalySystem
|
|||||||
SubscribeLocalEvent<AnomalyComponent, MapInitEvent>(OnMapInit);
|
SubscribeLocalEvent<AnomalyComponent, MapInitEvent>(OnMapInit);
|
||||||
SubscribeLocalEvent<AnomalyComponent, ComponentShutdown>(OnShutdown);
|
SubscribeLocalEvent<AnomalyComponent, ComponentShutdown>(OnShutdown);
|
||||||
SubscribeLocalEvent<AnomalyComponent, StartCollideEvent>(OnStartCollide);
|
SubscribeLocalEvent<AnomalyComponent, StartCollideEvent>(OnStartCollide);
|
||||||
|
SubscribeLocalEvent<AnomalyStabilityChangedEvent>(OnVesselAnomalyStabilityChanged);
|
||||||
|
|
||||||
InitializeGenerator();
|
InitializeGenerator();
|
||||||
InitializeScanner();
|
|
||||||
InitializeVessel();
|
InitializeVessel();
|
||||||
InitializeCommands();
|
InitializeCommands();
|
||||||
}
|
}
|
||||||
@@ -218,4 +216,112 @@ public sealed partial class AnomalySystem : SharedAnomalySystem
|
|||||||
EntityManager.RemoveComponents(anomaly, behavior.Components);
|
EntityManager.RemoveComponents(anomaly, behavior.Components);
|
||||||
}
|
}
|
||||||
#endregion
|
#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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,5 +36,13 @@ public sealed class SecretDataAnomalySystem : EntitySystem
|
|||||||
component.Secret.Add(_random.PickAndTake(_deita));
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ namespace Content.Server.Entry
|
|||||||
"LightFade",
|
"LightFade",
|
||||||
"HolidayRsiSwap",
|
"HolidayRsiSwap",
|
||||||
"OptionsVisualizer",
|
"OptionsVisualizer",
|
||||||
|
"AnomalyScannerScreen",
|
||||||
"MultipartMachineGhost"
|
"MultipartMachineGhost"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
using Content.Shared.Anomaly;
|
using Content.Shared.Anomaly;
|
||||||
using Robust.Shared.Audio;
|
using Robust.Shared.Audio;
|
||||||
|
using Robust.Shared.GameStates;
|
||||||
|
|
||||||
namespace Content.Server.Anomaly.Components;
|
namespace Content.Shared.Anomaly.Components;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This is used for scanning anomalies and
|
/// This is used for scanning anomalies and
|
||||||
/// displaying information about them in the ui
|
/// displaying information about them in the ui
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[RegisterComponent, Access(typeof(SharedAnomalySystem))]
|
[RegisterComponent, Access(typeof(SharedAnomalyScannerSystem))]
|
||||||
|
[NetworkedComponent]
|
||||||
public sealed partial class AnomalyScannerComponent : Component
|
public sealed partial class AnomalyScannerComponent : Component
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -19,12 +21,12 @@ public sealed partial class AnomalyScannerComponent : Component
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// How long the scan takes
|
/// How long the scan takes
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField("scanDoAfterDuration")]
|
[DataField]
|
||||||
public float ScanDoAfterDuration = 5;
|
public float ScanDoAfterDuration = 5;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The sound plays when the scan finished
|
/// The sound plays when the scan finished
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField("completeSound")]
|
[DataField]
|
||||||
public SoundSpecifier? CompleteSound = new SoundPathSpecifier("/Audio/Items/beep.ogg");
|
public SoundSpecifier? CompleteSound = new SoundPathSpecifier("/Audio/Items/beep.ogg");
|
||||||
}
|
}
|
||||||
@@ -17,6 +17,14 @@ public enum AnomalyVisualLayers : byte
|
|||||||
Animated
|
Animated
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public enum AnomalyStabilityVisuals : byte
|
||||||
|
{
|
||||||
|
Stable = 1,
|
||||||
|
Decaying = 2,
|
||||||
|
Growing = 3,
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The types of anomalous particles used
|
/// The types of anomalous particles used
|
||||||
/// for interfacing with anomalies.
|
/// for interfacing with anomalies.
|
||||||
@@ -41,7 +49,7 @@ public enum AnomalousParticleType : byte
|
|||||||
public enum AnomalyVesselVisuals : byte
|
public enum AnomalyVesselVisuals : byte
|
||||||
{
|
{
|
||||||
HasAnomaly,
|
HasAnomaly,
|
||||||
AnomalyState
|
AnomalySeverity,
|
||||||
}
|
}
|
||||||
|
|
||||||
[Serializable, NetSerializable]
|
[Serializable, NetSerializable]
|
||||||
@@ -68,6 +76,27 @@ public enum AnomalyScannerUiKey : byte
|
|||||||
Key
|
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]
|
[Serializable, NetSerializable]
|
||||||
public sealed class AnomalyScannerUserInterfaceState : BoundUserInterfaceState
|
public sealed class AnomalyScannerUserInterfaceState : BoundUserInterfaceState
|
||||||
{
|
{
|
||||||
|
|||||||
86
Content.Shared/Anomaly/SharedAnomalyScannerSystem.cs
Normal file
@@ -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;
|
||||||
|
|
||||||
|
/// <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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Content.Shared.Administration.Logs;
|
using Content.Shared.Administration.Logs;
|
||||||
using Content.Shared.Anomaly.Components;
|
using Content.Shared.Anomaly.Components;
|
||||||
using Content.Shared.Anomaly.Prototypes;
|
using Content.Shared.Anomaly.Prototypes;
|
||||||
@@ -140,6 +141,7 @@ public abstract class SharedAnomalySystem : EntitySystem
|
|||||||
var super = AddComp<AnomalySupercriticalComponent>(ent);
|
var super = AddComp<AnomalySupercriticalComponent>(ent);
|
||||||
super.EndTime = Timing.CurTime + ent.Comp.SupercriticalDuration;
|
super.EndTime = Timing.CurTime + ent.Comp.SupercriticalDuration;
|
||||||
Appearance.SetData(ent, AnomalyVisuals.Supercritical, true);
|
Appearance.SetData(ent, AnomalyVisuals.Supercritical, true);
|
||||||
|
SetScannerSupercritical((ent, ent.Comp), true);
|
||||||
Dirty(ent, super);
|
Dirty(ent, super);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,7 +342,8 @@ public abstract class SharedAnomalySystem : EntitySystem
|
|||||||
ChangeAnomalyHealth(ent, anomaly.HealthChangePerSecond * frameTime, anomaly);
|
ChangeAnomalyHealth(ent, anomaly.HealthChangePerSecond * frameTime, anomaly);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Timing.CurTime > anomaly.NextPulseTime)
|
var secondsUntilNextPulse = (anomaly.NextPulseTime - Timing.CurTime).TotalSeconds;
|
||||||
|
if (secondsUntilNextPulse < 0)
|
||||||
{
|
{
|
||||||
DoAnomalyPulse(ent, anomaly);
|
DoAnomalyPulse(ent, anomaly);
|
||||||
}
|
}
|
||||||
@@ -366,6 +369,18 @@ public abstract class SharedAnomalySystem : EntitySystem
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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>
|
/// <summary>
|
||||||
/// Gets random points around the anomaly based on the given parameters.
|
/// Gets random points around the anomaly based on the given parameters.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -441,6 +456,33 @@ public abstract class SharedAnomalySystem : EntitySystem
|
|||||||
}
|
}
|
||||||
return resultList;
|
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]
|
[DataRecord]
|
||||||
|
|||||||
@@ -6,7 +6,26 @@
|
|||||||
components:
|
components:
|
||||||
- type: Sprite
|
- type: Sprite
|
||||||
sprite: Objects/Specific/Research/anomalyscanner.rsi
|
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
|
- type: ActivatableUI
|
||||||
key: enum.AnomalyScannerUiKey.Key
|
key: enum.AnomalyScannerUiKey.Key
|
||||||
requireActiveHand: false
|
requireActiveHand: false
|
||||||
@@ -15,7 +34,35 @@
|
|||||||
interfaces:
|
interfaces:
|
||||||
enum.AnomalyScannerUiKey.Key:
|
enum.AnomalyScannerUiKey.Key:
|
||||||
type: AnomalyScannerBoundUserInterface
|
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: AnomalyScanner
|
||||||
|
- type: AnomalyScannerScreen
|
||||||
- type: GuideHelp
|
- type: GuideHelp
|
||||||
guides:
|
guides:
|
||||||
- ScannersAndVessels
|
- ScannersAndVessels
|
||||||
|
|||||||
@@ -53,15 +53,15 @@
|
|||||||
enum.AnomalyVesselVisualLayers.Base:
|
enum.AnomalyVesselVisualLayers.Base:
|
||||||
True: { visible: true }
|
True: { visible: true }
|
||||||
False: { visible: false }
|
False: { visible: false }
|
||||||
enum.AnomalyVesselVisuals.AnomalyState:
|
enum.AnomalyVesselVisuals.AnomalySeverity:
|
||||||
enum.PowerDeviceVisualLayers.Powered:
|
enum.PowerDeviceVisualLayers.Powered:
|
||||||
1: { state: powered-1 }
|
Stable: { state: powered-1 }
|
||||||
2: { state: powered-2 }
|
Decaying: { state: powered-2 }
|
||||||
3: { state: powered-3 }
|
Growing: { state: powered-3 }
|
||||||
enum.AnomalyVesselVisualLayers.Base:
|
enum.AnomalyVesselVisualLayers.Base:
|
||||||
1: { state: anomaly-1 }
|
Stable: { state: anomaly-1 }
|
||||||
2: { state: anomaly-2 }
|
Decaying: { state: anomaly-2 }
|
||||||
3: { state: anomaly-3 }
|
Growing: { state: anomaly-3 }
|
||||||
enum.WiresVisuals.MaintenancePanelState:
|
enum.WiresVisuals.MaintenancePanelState:
|
||||||
enum.WiresVisualLayers.MaintenancePanel:
|
enum.WiresVisualLayers.MaintenancePanel:
|
||||||
True: { visible: false }
|
True: { visible: false }
|
||||||
|
|||||||
|
After Width: | Height: | Size: 603 B |
|
After Width: | Height: | Size: 211 B |
@@ -17,6 +17,45 @@
|
|||||||
{
|
{
|
||||||
"name": "inhand-right",
|
"name": "inhand-right",
|
||||||
"directions": 4
|
"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 ]
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
|
After Width: | Height: | Size: 195 B |
|
After Width: | Height: | Size: 501 B |
|
After Width: | Height: | Size: 149 B |
|
After Width: | Height: | Size: 157 B |
|
After Width: | Height: | Size: 158 B |
|
After Width: | Height: | Size: 158 B |
|
After Width: | Height: | Size: 160 B |