Anomalies (#13371)
59
Content.Client/Anomaly/AnomalySystem.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using Content.Shared.Anomaly;
|
||||
using Content.Shared.Anomaly.Components;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.Anomaly;
|
||||
|
||||
public sealed class AnomalySystem : SharedAnomalySystem
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<AnomalyComponent, AppearanceChangeEvent>(OnAppearanceChanged);
|
||||
}
|
||||
|
||||
private void OnAppearanceChanged(EntityUid uid, AnomalyComponent component, ref AppearanceChangeEvent args)
|
||||
{
|
||||
if (args.Sprite is not { } sprite)
|
||||
return;
|
||||
|
||||
if (!Appearance.TryGetData(uid, AnomalyVisuals.IsPulsing, out bool pulsing, args.Component))
|
||||
pulsing = false;
|
||||
|
||||
if (Appearance.TryGetData(uid, AnomalyVisuals.IsPulsing, out bool super, args.Component) && super)
|
||||
pulsing = super;
|
||||
|
||||
if (HasComp<AnomalySupercriticalComponent>(uid))
|
||||
pulsing = true;
|
||||
|
||||
if (!sprite.LayerMapTryGet(AnomalyVisualLayers.Base, out var layer) ||
|
||||
!sprite.LayerMapTryGet(AnomalyVisualLayers.Animated, out var animatedLayer))
|
||||
return;
|
||||
|
||||
sprite.LayerSetVisible(layer, !pulsing);
|
||||
sprite.LayerSetVisible(animatedLayer, pulsing);
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
foreach (var (super, sprite) in EntityQuery<AnomalySupercriticalComponent, SpriteComponent>())
|
||||
{
|
||||
var completion = 1f - (float) ((super.EndTime - _timing.CurTime) / super.SupercriticalDuration);
|
||||
var scale = completion * (super.MaxScaleAmount - 1f) + 1f;
|
||||
sprite.Scale = new Vector2(scale, scale);
|
||||
|
||||
var transparency = (byte) (65 * (1f - completion) + 190);
|
||||
if (transparency < sprite.Color.AByte)
|
||||
{
|
||||
sprite.Color = sprite.Color.WithAlpha(transparency);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
8
Content.Client/Anomaly/Effects/GravityAnomalySystem.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using Content.Shared.Anomaly.Effects;
|
||||
|
||||
namespace Content.Client.Anomaly.Effects;
|
||||
|
||||
public sealed class GravityAnomalySystem : SharedGravityAnomalySystem
|
||||
{
|
||||
// this is not the system you are looking for
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using Content.Shared.Anomaly;
|
||||
using Content.Shared.Gravity;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.GameObjects;
|
||||
|
||||
namespace Content.Client. Anomaly.Ui;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class AnomalyGeneratorBoundUserInterface : BoundUserInterface
|
||||
{
|
||||
private AnomalyGeneratorWindow? _window;
|
||||
|
||||
public AnomalyGeneratorBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base (owner, uiKey)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void Open()
|
||||
{
|
||||
base.Open();
|
||||
|
||||
_window = new (Owner.Owner);
|
||||
|
||||
_window.OpenCentered();
|
||||
_window.OnClose += Close;
|
||||
|
||||
_window.OnGenerateButtonPressed += () =>
|
||||
{
|
||||
SendMessage(new AnomalyGeneratorGenerateButtonPressedEvent());
|
||||
};
|
||||
}
|
||||
|
||||
protected override void UpdateState(BoundUserInterfaceState state)
|
||||
{
|
||||
base.UpdateState(state);
|
||||
|
||||
if (state is not AnomalyGeneratorUserInterfaceState msg)
|
||||
return;
|
||||
_window?.UpdateState(msg);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
if (!disposing) return;
|
||||
|
||||
_window?.Dispose();
|
||||
}
|
||||
|
||||
public void SetPowerSwitch(bool on)
|
||||
{
|
||||
SendMessage(new SharedGravityGeneratorComponent.SwitchGeneratorMessage(on));
|
||||
}
|
||||
}
|
||||
|
||||
48
Content.Client/Anomaly/Ui/AnomalyGeneratorWindow.xaml
Normal file
@@ -0,0 +1,48 @@
|
||||
<controls:FancyWindow xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
Title="{Loc 'anomaly-generator-ui-title'}"
|
||||
MinSize="270 180"
|
||||
SetSize="360 180">
|
||||
<BoxContainer Margin="10 0 10 0"
|
||||
Orientation="Vertical"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True">
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True"
|
||||
Margin="0 0 0 0"
|
||||
VerticalAlignment="Center">
|
||||
<Label Text="{Loc 'anomaly-generator-fuel-display'}" StyleClasses="StatusFieldTitle" />
|
||||
<ProgressBar Name="FuelBar"
|
||||
HorizontalExpand="True"
|
||||
MaxValue="1"
|
||||
MinValue="0"
|
||||
SetHeight="25"
|
||||
Margin="10 0 10 0"
|
||||
VerticalAlignment="Center">
|
||||
<Label Name="FuelText"
|
||||
Margin="4 0"
|
||||
Text="0 %" />
|
||||
</ProgressBar>
|
||||
</BoxContainer>
|
||||
<RichTextLabel Name="CooldownLabel" StyleClasses="StatusFieldTitle" />
|
||||
<RichTextLabel Name="ReadyLabel" StyleClasses="StatusFieldTitle" />
|
||||
</BoxContainer>
|
||||
<PanelContainer Margin="12 0 0 0"
|
||||
StyleClasses="Inset"
|
||||
VerticalAlignment="Center">
|
||||
<SpriteView Name="EntityView"
|
||||
SetSize="96 96"
|
||||
OverrideDirection="South" />
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
<BoxContainer VerticalExpand="True"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center">
|
||||
<Button Name="GenerateButton"
|
||||
Text="{Loc 'anomaly-generator-generate'}"></Button>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</controls:FancyWindow>
|
||||
80
Content.Client/Anomaly/Ui/AnomalyGeneratorWindow.xaml.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
using Content.Client.Message;
|
||||
using Content.Shared.Anomaly;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Timing;
|
||||
using FancyWindow = Content.Client.UserInterface.Controls.FancyWindow;
|
||||
|
||||
namespace Content.Client.Anomaly.Ui;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class AnomalyGeneratorWindow : FancyWindow
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
|
||||
private TimeSpan _cooldownEnd = TimeSpan.Zero;
|
||||
private bool _hasEnoughFuel;
|
||||
|
||||
public Action? OnGenerateButtonPressed;
|
||||
|
||||
public AnomalyGeneratorWindow(EntityUid gen)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
EntityView.Sprite = _entityManager.GetComponent<SpriteComponent>(gen);
|
||||
|
||||
GenerateButton.OnPressed += _ => OnGenerateButtonPressed?.Invoke();
|
||||
}
|
||||
|
||||
public void UpdateState(AnomalyGeneratorUserInterfaceState state)
|
||||
{
|
||||
_cooldownEnd = state.CooldownEndTime;
|
||||
_hasEnoughFuel = state.FuelCost <= state.FuelAmount;
|
||||
|
||||
var fuelCompletion = Math.Clamp((float) state.FuelAmount / state.FuelCost, 0f, 1f);
|
||||
|
||||
FuelBar.Value = fuelCompletion;
|
||||
FuelText.Text = $"{fuelCompletion:P}";
|
||||
|
||||
UpdateTimer();
|
||||
UpdateReady(); // yes this can trigger twice. no i don't care
|
||||
}
|
||||
|
||||
public void UpdateTimer()
|
||||
{
|
||||
if (_timing.CurTime > _cooldownEnd)
|
||||
{
|
||||
CooldownLabel.SetMarkup(Loc.GetString("anomaly-generator-no-cooldown"));
|
||||
}
|
||||
else
|
||||
{
|
||||
var timeLeft = _cooldownEnd - _timing.CurTime;
|
||||
var timeString = $"{timeLeft.Minutes:0}:{timeLeft.Seconds:00}";
|
||||
CooldownLabel.SetMarkup(Loc.GetString("anomaly-generator-cooldown", ("time", timeString)));
|
||||
UpdateReady();
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateReady()
|
||||
{
|
||||
var ready = _hasEnoughFuel && _timing.CurTime > _cooldownEnd;
|
||||
|
||||
var msg = ready
|
||||
? Loc.GetString("anomaly-generator-yes-fire")
|
||||
: Loc.GetString("anomaly-generator-no-fire");
|
||||
ReadyLabel.SetMarkup(msg);
|
||||
|
||||
GenerateButton.Disabled = !ready;
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
|
||||
UpdateTimer();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
using Content.Shared.Anomaly;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.GameObjects;
|
||||
|
||||
namespace Content.Client.Anomaly.Ui;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class AnomalyScannerBoundUserInterface : BoundUserInterface
|
||||
{
|
||||
private AnomalyScannerMenu? _menu;
|
||||
|
||||
public AnomalyScannerBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
protected override void Open()
|
||||
{
|
||||
base.Open();
|
||||
|
||||
_menu = new AnomalyScannerMenu();
|
||||
_menu.OpenCentered();
|
||||
}
|
||||
|
||||
protected override void UpdateState(BoundUserInterfaceState state)
|
||||
{
|
||||
base.UpdateState(state);
|
||||
|
||||
if (state is not AnomalyScannerUserInterfaceState msg)
|
||||
return;
|
||||
|
||||
if (_menu == null)
|
||||
return;
|
||||
|
||||
_menu.LastMessage = msg.Message;
|
||||
_menu.NextPulseTime = msg.NextPulseTime;
|
||||
_menu.UpdateMenu();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
if (!disposing)
|
||||
return;
|
||||
_menu?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
12
Content.Client/Anomaly/Ui/AnomalyScannerMenu.xaml
Normal file
@@ -0,0 +1,12 @@
|
||||
<controls:FancyWindow
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
Title="{Loc 'anomaly-scanner-ui-title'}"
|
||||
MinSize="350 260"
|
||||
SetSize="350 260">
|
||||
<BoxContainer Orientation="Vertical" VerticalExpand="True" Margin="10 0 10 10">
|
||||
<RichTextLabel Name="TextDisplay"></RichTextLabel>
|
||||
</BoxContainer>
|
||||
</controls:FancyWindow>
|
||||
|
||||
|
||||
47
Content.Client/Anomaly/Ui/AnomalyScannerMenu.xaml.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using Content.Client.Message;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Anomaly.Ui;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class AnomalyScannerMenu : FancyWindow
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
|
||||
public FormattedMessage LastMessage = new();
|
||||
public TimeSpan? NextPulseTime;
|
||||
|
||||
public AnomalyScannerMenu()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
}
|
||||
|
||||
public void UpdateMenu()
|
||||
{
|
||||
var msg = new FormattedMessage(LastMessage);
|
||||
|
||||
if (NextPulseTime != null)
|
||||
{
|
||||
msg.PushNewline();
|
||||
msg.PushNewline();
|
||||
var time = NextPulseTime.Value - _timing.CurTime;
|
||||
var timestring = $"{time.Minutes:00}:{time.Seconds:00}";
|
||||
msg.AddMarkup(Loc.GetString("anomaly-scanner-pulse-timer", ("time", timestring)));
|
||||
}
|
||||
|
||||
TextDisplay.SetMarkup(msg.ToMarkup());
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
|
||||
if (NextPulseTime != null)
|
||||
UpdateMenu();
|
||||
}
|
||||
}
|
||||
114
Content.Server/Anomaly/AnomalySystem.Generator.cs
Normal file
@@ -0,0 +1,114 @@
|
||||
using Content.Server.Anomaly.Components;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Server.Power.EntitySystems;
|
||||
using Content.Shared.Anomaly;
|
||||
using Content.Shared.Materials;
|
||||
using Robust.Shared.Map.Components;
|
||||
|
||||
namespace Content.Server.Anomaly;
|
||||
|
||||
/// <summary>
|
||||
/// This handles anomalous vessel as well as
|
||||
/// the calculations for how many points they
|
||||
/// should produce.
|
||||
/// </summary>
|
||||
public sealed partial class AnomalySystem
|
||||
{
|
||||
/// <summary>
|
||||
/// A multiplier applied to the grid bounds
|
||||
/// to make the likelihood of it spawning outside
|
||||
/// of the main station less likely.
|
||||
///
|
||||
/// tl;dr anomalies only generate on the inner __% of the station.
|
||||
/// </summary>
|
||||
public const float GridBoundsMultiplier = 0.6f;
|
||||
|
||||
private void InitializeGenerator()
|
||||
{
|
||||
SubscribeLocalEvent<AnomalyGeneratorComponent, BoundUIOpenedEvent>(OnGeneratorBUIOpened);
|
||||
SubscribeLocalEvent<AnomalyGeneratorComponent, MaterialAmountChangedEvent>(OnGeneratorMaterialAmountChanged);
|
||||
SubscribeLocalEvent<AnomalyGeneratorComponent, AnomalyGeneratorGenerateButtonPressedEvent>(OnGenerateButtonPressed);
|
||||
SubscribeLocalEvent<AnomalyGeneratorComponent, PowerChangedEvent>(OnGeneratorPowerChanged);
|
||||
}
|
||||
|
||||
private void OnGeneratorPowerChanged(EntityUid uid, AnomalyGeneratorComponent component, ref PowerChangedEvent args)
|
||||
{
|
||||
_ambient.SetAmbience(uid, args.Powered);
|
||||
}
|
||||
|
||||
private void OnGeneratorBUIOpened(EntityUid uid, AnomalyGeneratorComponent component, BoundUIOpenedEvent args)
|
||||
{
|
||||
UpdateGeneratorUi(uid, component);
|
||||
}
|
||||
|
||||
private void OnGeneratorMaterialAmountChanged(EntityUid uid, AnomalyGeneratorComponent component, ref MaterialAmountChangedEvent args)
|
||||
{
|
||||
UpdateGeneratorUi(uid, component);
|
||||
}
|
||||
|
||||
private void OnGenerateButtonPressed(EntityUid uid, AnomalyGeneratorComponent component, AnomalyGeneratorGenerateButtonPressedEvent args)
|
||||
{
|
||||
TryGeneratorCreateAnomaly(uid, component);
|
||||
}
|
||||
|
||||
public void UpdateGeneratorUi(EntityUid uid, AnomalyGeneratorComponent component)
|
||||
{
|
||||
var materialAmount = _material.GetMaterialAmount(uid, component.RequiredMaterial);
|
||||
|
||||
var state = new AnomalyGeneratorUserInterfaceState(component.CooldownEndTime, materialAmount, component.MaterialPerAnomaly);
|
||||
_ui.TrySetUiState(uid, AnomalyGeneratorUiKey.Key, state);
|
||||
}
|
||||
|
||||
public void TryGeneratorCreateAnomaly(EntityUid uid, AnomalyGeneratorComponent? component = null)
|
||||
{
|
||||
if (!Resolve(uid, ref component))
|
||||
return;
|
||||
|
||||
if (!this.IsPowered(uid, EntityManager))
|
||||
return;
|
||||
|
||||
if (Timing.CurTime < component.CooldownEndTime)
|
||||
return;
|
||||
|
||||
var grid = Transform(uid).GridUid;
|
||||
if (grid == null)
|
||||
return;
|
||||
|
||||
if (!_material.TryChangeMaterialAmount(uid, component.RequiredMaterial, -component.MaterialPerAnomaly))
|
||||
return;
|
||||
|
||||
SpawnOnRandomGridLocation(grid.Value, component.SpawnerPrototype);
|
||||
component.CooldownEndTime = Timing.CurTime + component.CooldownLength;
|
||||
UpdateGeneratorUi(uid, component);
|
||||
}
|
||||
|
||||
private void SpawnOnRandomGridLocation(EntityUid grid, string toSpawn)
|
||||
{
|
||||
if (!TryComp<MapGridComponent>(grid, out var gridComp))
|
||||
return;
|
||||
|
||||
var xform = Transform(grid);
|
||||
|
||||
var targetCoords = xform.Coordinates;
|
||||
var (gridPos, _, gridMatrix) = _transform.GetWorldPositionRotationMatrix(xform);
|
||||
var gridBounds = gridMatrix.TransformBox(gridComp.LocalAABB);
|
||||
|
||||
for (var i = 0; i < 25; i++)
|
||||
{
|
||||
var randomX = Random.Next((int) (gridBounds.Left * GridBoundsMultiplier), (int) (gridBounds.Right * GridBoundsMultiplier));
|
||||
var randomY = Random.Next((int) (gridBounds.Bottom * GridBoundsMultiplier), (int) (gridBounds.Top * GridBoundsMultiplier));
|
||||
|
||||
var tile = new Vector2i(randomX - (int) gridPos.X, randomY - (int) gridPos.Y);
|
||||
if (_atmosphere.IsTileSpace(grid, Transform(grid).MapUid, tile,
|
||||
mapGridComp: gridComp) || _atmosphere.IsTileAirBlocked(grid, tile, mapGridComp: gridComp))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
targetCoords = gridComp.GridTileToLocal(tile);
|
||||
break;
|
||||
}
|
||||
|
||||
Spawn(toSpawn, targetCoords);
|
||||
}
|
||||
}
|
||||
169
Content.Server/Anomaly/AnomalySystem.Scanner.cs
Normal file
@@ -0,0 +1,169 @@
|
||||
using Content.Server.Anomaly.Components;
|
||||
using Content.Server.DoAfter;
|
||||
using Content.Shared.Anomaly;
|
||||
using Content.Shared.Anomaly.Components;
|
||||
using Content.Shared.Interaction;
|
||||
using Robust.Server.GameObjects;
|
||||
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, AnomalyScanFinishedEvent>(OnScannerDoAfterFinished);
|
||||
SubscribeLocalEvent<AnomalyScannerComponent, AnomalyScanCancelledEvent>(OnScannerDoAfterCancelled);
|
||||
|
||||
SubscribeLocalEvent<AnomalyShutdownEvent>(OnScannerAnomalyShutdown);
|
||||
SubscribeLocalEvent<AnomalySeverityChangedEvent>(OnScannerAnomalySeverityChanged);
|
||||
SubscribeLocalEvent<AnomalyStabilityChangedEvent>(OnScannerAnomalyStabilityChanged);
|
||||
SubscribeLocalEvent<AnomalyHealthChangedEvent>(OnScannerAnomalyHealthChanged);
|
||||
}
|
||||
|
||||
private void OnScannerAnomalyShutdown(ref AnomalyShutdownEvent args)
|
||||
{
|
||||
foreach (var component in EntityQuery<AnomalyScannerComponent>())
|
||||
{
|
||||
if (component.ScannedAnomaly != args.Anomaly)
|
||||
continue;
|
||||
_ui.TryCloseAll(component.Owner, AnomalyScannerUiKey.Key);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnScannerAnomalySeverityChanged(ref AnomalySeverityChangedEvent args)
|
||||
{
|
||||
foreach (var component in EntityQuery<AnomalyScannerComponent>())
|
||||
{
|
||||
if (component.ScannedAnomaly != args.Anomaly)
|
||||
continue;
|
||||
UpdateScannerUi(component.Owner, component);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnScannerAnomalyStabilityChanged(ref AnomalyStabilityChangedEvent args)
|
||||
{
|
||||
foreach (var component in EntityQuery<AnomalyScannerComponent>())
|
||||
{
|
||||
if (component.ScannedAnomaly != args.Anomaly)
|
||||
continue;
|
||||
UpdateScannerUi(component.Owner, component);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnScannerAnomalyHealthChanged(ref AnomalyHealthChangedEvent args)
|
||||
{
|
||||
foreach (var component in EntityQuery<AnomalyScannerComponent>())
|
||||
{
|
||||
if (component.ScannedAnomaly != args.Anomaly)
|
||||
continue;
|
||||
UpdateScannerUi(component.Owner, component);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnScannerUiOpened(EntityUid uid, AnomalyScannerComponent component, BoundUIOpenedEvent args)
|
||||
{
|
||||
UpdateScannerUi(uid, component);
|
||||
}
|
||||
|
||||
private void OnScannerAfterInteract(EntityUid uid, AnomalyScannerComponent component, AfterInteractEvent args)
|
||||
{
|
||||
if (component.TokenSource != null)
|
||||
return;
|
||||
|
||||
if (args.Target is not { } target)
|
||||
return;
|
||||
if (!HasComp<AnomalyComponent>(target))
|
||||
return;
|
||||
|
||||
component.TokenSource = new();
|
||||
_doAfter.DoAfter(new DoAfterEventArgs(args.User, component.ScanDoAfterDuration, component.TokenSource.Token, target, uid)
|
||||
{
|
||||
DistanceThreshold = 2f,
|
||||
UsedFinishedEvent = new AnomalyScanFinishedEvent(target, args.User),
|
||||
UsedCancelledEvent = new AnomalyScanCancelledEvent()
|
||||
});
|
||||
}
|
||||
|
||||
private void OnScannerDoAfterFinished(EntityUid uid, AnomalyScannerComponent component, AnomalyScanFinishedEvent args)
|
||||
{
|
||||
component.TokenSource = null;
|
||||
|
||||
Audio.PlayPvs(component.CompleteSound, uid);
|
||||
_popup.PopupEntity(Loc.GetString("anomaly-scanner-component-scan-complete"), uid);
|
||||
UpdateScannerWithNewAnomaly(uid, args.Anomaly, component);
|
||||
|
||||
if (TryComp<ActorComponent>(args.User, out var actor))
|
||||
_ui.TryOpen(uid, AnomalyScannerUiKey.Key, actor.PlayerSession);
|
||||
}
|
||||
|
||||
private void OnScannerDoAfterCancelled(EntityUid uid, AnomalyScannerComponent component, AnomalyScanCancelledEvent args)
|
||||
{
|
||||
component.TokenSource = null;
|
||||
}
|
||||
|
||||
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.TrySetUiState(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.AddMarkup(Loc.GetString("anomaly-scanner-no-anomaly"));
|
||||
return msg;
|
||||
}
|
||||
|
||||
msg.AddMarkup(Loc.GetString("anomaly-scanner-severity-percentage", ("percent", anomalyComp.Severity.ToString("P"))));
|
||||
msg.PushNewline();
|
||||
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.AddMarkup(stateLoc);
|
||||
msg.PushNewline();
|
||||
|
||||
var points = GetAnomalyPointValue(anomaly, anomalyComp) / 10 * 10; //round to tens place
|
||||
msg.AddMarkup(Loc.GetString("anomaly-scanner-point-output", ("point", points)));
|
||||
msg.PushNewline();
|
||||
msg.PushNewline();
|
||||
|
||||
msg.AddMarkup(Loc.GetString("anomaly-scanner-particle-readout"));
|
||||
msg.PushNewline();
|
||||
msg.AddMarkup(Loc.GetString("anomaly-scanner-particle-danger", ("type", GetParticleLocale(anomalyComp.SeverityParticleType))));
|
||||
msg.PushNewline();
|
||||
msg.AddMarkup(Loc.GetString("anomaly-scanner-particle-unstable", ("type", GetParticleLocale(anomalyComp.DestabilizingParticleType))));
|
||||
msg.PushNewline();
|
||||
msg.AddMarkup(Loc.GetString("anomaly-scanner-particle-containment", ("type", GetParticleLocale(anomalyComp.WeakeningParticleType))));
|
||||
|
||||
//The timer at the end here is actually added in the ui itself.
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
129
Content.Server/Anomaly/AnomalySystem.Vessel.cs
Normal file
@@ -0,0 +1,129 @@
|
||||
using Content.Server.Anomaly.Components;
|
||||
using Content.Server.Construction;
|
||||
using Content.Server.Power.EntitySystems;
|
||||
using Content.Shared.Anomaly;
|
||||
using Content.Shared.Anomaly.Components;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Research.Components;
|
||||
|
||||
namespace Content.Server.Anomaly;
|
||||
|
||||
/// <summary>
|
||||
/// This handles anomalous vessel as well as
|
||||
/// the calculations for how many points they
|
||||
/// should produce.
|
||||
/// </summary>
|
||||
public sealed partial class AnomalySystem
|
||||
{
|
||||
private void InitializeVessel()
|
||||
{
|
||||
SubscribeLocalEvent<AnomalyVesselComponent, ComponentShutdown>(OnVesselShutdown);
|
||||
SubscribeLocalEvent<AnomalyVesselComponent, MapInitEvent>(OnVesselMapInit);
|
||||
SubscribeLocalEvent<AnomalyVesselComponent, RefreshPartsEvent>(OnRefreshParts);
|
||||
SubscribeLocalEvent<AnomalyVesselComponent, InteractUsingEvent>(OnVesselInteractUsing);
|
||||
SubscribeLocalEvent<AnomalyVesselComponent, ExaminedEvent>(OnExamined);
|
||||
SubscribeLocalEvent<AnomalyVesselComponent, ResearchServerGetPointsPerSecondEvent>(OnVesselGetPointsPerSecond);
|
||||
SubscribeLocalEvent<AnomalyShutdownEvent>(OnVesselAnomalyShutdown);
|
||||
}
|
||||
|
||||
private void OnExamined(EntityUid uid, AnomalyVesselComponent component, ExaminedEvent args)
|
||||
{
|
||||
if (!args.IsInDetailsRange)
|
||||
return;
|
||||
|
||||
args.PushText(component.Anomaly == null
|
||||
? Loc.GetString("anomaly-vessel-component-not-assigned")
|
||||
: Loc.GetString("anomaly-vessel-component-assigned"));
|
||||
}
|
||||
|
||||
private void OnVesselShutdown(EntityUid uid, AnomalyVesselComponent component, ComponentShutdown args)
|
||||
{
|
||||
if (component.Anomaly is not { } anomaly)
|
||||
return;
|
||||
|
||||
if (!TryComp<AnomalyComponent>(anomaly, out var anomalyComp))
|
||||
return;
|
||||
|
||||
anomalyComp.ConnectedVessel = null;
|
||||
}
|
||||
|
||||
private void OnVesselMapInit(EntityUid uid, AnomalyVesselComponent component, MapInitEvent args)
|
||||
{
|
||||
UpdateVesselAppearance(uid, component);
|
||||
}
|
||||
|
||||
private void OnRefreshParts(EntityUid uid, AnomalyVesselComponent component, RefreshPartsEvent args)
|
||||
{
|
||||
var modifierRating = args.PartRatings[component.MachinePartPointModifier] - 1;
|
||||
component.PointMultiplier = MathF.Pow(component.PartRatingPointModifier, modifierRating);
|
||||
}
|
||||
|
||||
private void OnVesselInteractUsing(EntityUid uid, AnomalyVesselComponent component, InteractUsingEvent args)
|
||||
{
|
||||
if (component.Anomaly != null ||
|
||||
!TryComp<AnomalyScannerComponent>(args.Used, out var scanner) ||
|
||||
scanner.ScannedAnomaly is not {} anomaly)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TryComp<AnomalyComponent>(anomaly, out var anomalyComponent) || anomalyComponent.ConnectedVessel != null)
|
||||
return;
|
||||
|
||||
component.Anomaly = scanner.ScannedAnomaly;
|
||||
anomalyComponent.ConnectedVessel = uid;
|
||||
UpdateVesselAppearance(uid, component);
|
||||
_popup.PopupEntity(Loc.GetString("anomaly-vessel-component-anomaly-assigned"), uid);
|
||||
}
|
||||
|
||||
private void OnVesselGetPointsPerSecond(EntityUid uid, AnomalyVesselComponent component, ref ResearchServerGetPointsPerSecondEvent args)
|
||||
{
|
||||
if (!this.IsPowered(uid, EntityManager) || component.Anomaly is not {} anomaly)
|
||||
{
|
||||
args.Points = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
args.Points += (int) (GetAnomalyPointValue(anomaly) * component.PointMultiplier);
|
||||
}
|
||||
|
||||
private void OnVesselAnomalyShutdown(ref AnomalyShutdownEvent args)
|
||||
{
|
||||
foreach (var component in EntityQuery<AnomalyVesselComponent>())
|
||||
{
|
||||
var ent = component.Owner;
|
||||
|
||||
if (args.Anomaly != component.Anomaly)
|
||||
continue;
|
||||
|
||||
component.Anomaly = null;
|
||||
UpdateVesselAppearance(ent, component);
|
||||
|
||||
if (!args.Supercritical)
|
||||
continue;
|
||||
_explosion.TriggerExplosive(ent);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the appearance of an anomaly vessel
|
||||
/// based on whether or not it has an anomaly
|
||||
/// </summary>
|
||||
/// <param name="uid"></param>
|
||||
/// <param name="component"></param>
|
||||
public void UpdateVesselAppearance(EntityUid uid, AnomalyVesselComponent? component = null)
|
||||
{
|
||||
if (!Resolve(uid, ref component))
|
||||
return;
|
||||
|
||||
var on = component.Anomaly != null;
|
||||
|
||||
Appearance.SetData(uid, AnomalyVesselVisuals.HasAnomaly, on);
|
||||
if (TryComp<SharedPointLightComponent>(uid, out var pointLightComponent))
|
||||
{
|
||||
pointLightComponent.Enabled = on;
|
||||
}
|
||||
_ambient.SetAmbience(uid, on);
|
||||
}
|
||||
}
|
||||
127
Content.Server/Anomaly/AnomalySystem.cs
Normal file
@@ -0,0 +1,127 @@
|
||||
using Content.Server.Anomaly.Components;
|
||||
using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Server.Audio;
|
||||
using Content.Server.DoAfter;
|
||||
using Content.Server.Explosion.EntitySystems;
|
||||
using Content.Server.Materials;
|
||||
using Content.Server.Popups;
|
||||
using Content.Shared.Anomaly;
|
||||
using Content.Shared.Anomaly.Components;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Physics.Events;
|
||||
using Robust.Shared.Random;
|
||||
namespace Content.Server.Anomaly;
|
||||
|
||||
/// <summary>
|
||||
/// This handles logic and interactions relating to <see cref="AnomalyComponent"/>
|
||||
/// </summary>
|
||||
public sealed partial class AnomalySystem : SharedAnomalySystem
|
||||
{
|
||||
[Dependency] private readonly AmbientSoundSystem _ambient = default!;
|
||||
[Dependency] private readonly AtmosphereSystem _atmosphere = default!;
|
||||
[Dependency] private readonly DoAfterSystem _doAfter = default!;
|
||||
[Dependency] private readonly ExplosionSystem _explosion = default!;
|
||||
[Dependency] private readonly MaterialStorageSystem _material = default!;
|
||||
[Dependency] private readonly PopupSystem _popup = default!;
|
||||
[Dependency] private readonly TransformSystem _transform = default!;
|
||||
[Dependency] private readonly UserInterfaceSystem _ui = default!;
|
||||
|
||||
public const float MinParticleVariation = 0.8f;
|
||||
public const float MaxParticleVariation = 1.2f;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<AnomalyComponent, MapInitEvent>(OnMapInit);
|
||||
SubscribeLocalEvent<AnomalyComponent, ComponentShutdown>(OnShutdown);
|
||||
SubscribeLocalEvent<AnomalyComponent, StartCollideEvent>(OnStartCollide);
|
||||
|
||||
InitializeGenerator();
|
||||
InitializeScanner();
|
||||
InitializeVessel();
|
||||
}
|
||||
|
||||
private void OnMapInit(EntityUid uid, AnomalyComponent component, MapInitEvent args)
|
||||
{
|
||||
component.NextPulseTime = Timing.CurTime + GetPulseLength(component) * 3; // longer the first time
|
||||
ChangeAnomalyStability(uid, Random.NextFloat(component.InitialStabilityRange.Item1 , component.InitialStabilityRange.Item2), component);
|
||||
ChangeAnomalySeverity(uid, Random.NextFloat(component.InitialSeverityRange.Item1, component.InitialSeverityRange.Item2), component);
|
||||
|
||||
var particles = new List<AnomalousParticleType>
|
||||
{ AnomalousParticleType.Delta, AnomalousParticleType.Epsilon, AnomalousParticleType.Zeta };
|
||||
component.SeverityParticleType = Random.PickAndTake(particles);
|
||||
component.DestabilizingParticleType = Random.PickAndTake(particles);
|
||||
component.WeakeningParticleType = Random.PickAndTake(particles);
|
||||
}
|
||||
|
||||
private void OnShutdown(EntityUid uid, AnomalyComponent component, ComponentShutdown args)
|
||||
{
|
||||
EndAnomaly(uid, component);
|
||||
}
|
||||
|
||||
private void OnStartCollide(EntityUid uid, AnomalyComponent component, ref StartCollideEvent args)
|
||||
{
|
||||
if (!TryComp<AnomalousParticleComponent>(args.OtherFixture.Body.Owner, out var particleComponent))
|
||||
return;
|
||||
|
||||
if (args.OtherFixture.ID != particleComponent.FixtureId)
|
||||
return;
|
||||
|
||||
// small function to randomize because it's easier to read like this
|
||||
float VaryValue(float v) => v * Random.NextFloat(MinParticleVariation, MaxParticleVariation);
|
||||
|
||||
if (particleComponent.ParticleType == component.DestabilizingParticleType)
|
||||
{
|
||||
ChangeAnomalyStability(uid, VaryValue(component.StabilityPerDestabilizingHit), component);
|
||||
}
|
||||
else if (particleComponent.ParticleType == component.SeverityParticleType)
|
||||
{
|
||||
ChangeAnomalySeverity(uid, VaryValue(component.SeverityPerSeverityHit), component);
|
||||
}
|
||||
else if (particleComponent.ParticleType == component.WeakeningParticleType)
|
||||
{
|
||||
ChangeAnomalyHealth(uid, VaryValue(component.HealthPerWeakeningeHit), component);
|
||||
ChangeAnomalyStability(uid, VaryValue(component.StabilityPerWeakeningeHit), component);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the amount of research points generated per second for an anomaly.
|
||||
/// </summary>
|
||||
/// <param name="anomaly"></param>
|
||||
/// <param name="component"></param>
|
||||
/// <returns>The amount of points</returns>
|
||||
public int GetAnomalyPointValue(EntityUid anomaly, AnomalyComponent? component = null)
|
||||
{
|
||||
if (!Resolve(anomaly, ref component, false))
|
||||
return 0;
|
||||
|
||||
var multiplier = 1f;
|
||||
if (component.Stability > component.GrowthThreshold)
|
||||
multiplier = 1.25f; //more points for unstable
|
||||
else if (component.Stability < component.DecayThreshold)
|
||||
multiplier = 0.75f; //less points if it's dying
|
||||
|
||||
//penalty of up to 50% based on health
|
||||
multiplier *= MathF.Pow(1.5f, component.Health) - 0.5f;
|
||||
|
||||
return (int) ((component.MaxPointsPerSecond - component.MinPointsPerSecond) * component.Severity * multiplier);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the localized name of a particle.
|
||||
/// </summary>
|
||||
/// <param name="type"></param>
|
||||
/// <returns></returns>
|
||||
public string GetParticleLocale(AnomalousParticleType type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
AnomalousParticleType.Delta => Loc.GetString("anomaly-particles-delta"),
|
||||
AnomalousParticleType.Epsilon => Loc.GetString("anomaly-particles-epsilon"),
|
||||
AnomalousParticleType.Zeta => Loc.GetString("anomaly-particles-zeta"),
|
||||
_ => throw new ArgumentOutOfRangeException()
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using Content.Shared.Anomaly;
|
||||
|
||||
namespace Content.Server.Anomaly.Components;
|
||||
|
||||
/// <summary>
|
||||
/// This is used for projectiles which affect anomalies through colliding with them.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed class AnomalousParticleComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of particle that the projectile
|
||||
/// imbues onto the anomaly on contact.
|
||||
/// </summary>
|
||||
[DataField("particleType", required: true)]
|
||||
public AnomalousParticleType ParticleType;
|
||||
|
||||
/// <summary>
|
||||
/// The fixture that's checked on collision.
|
||||
/// </summary>
|
||||
[DataField("fixtureId")]
|
||||
public string FixtureId = "projectile";
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using Content.Shared.Materials;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
namespace Content.Server.Anomaly.Components;
|
||||
|
||||
/// <summary>
|
||||
/// This is used for a machine that is able to generate
|
||||
/// anomalies randomly on the station.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed class AnomalyGeneratorComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The time at which the cooldown for generating another anomaly will be over
|
||||
/// </summary>
|
||||
[DataField("cooldownEndTime", customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)]
|
||||
public TimeSpan CooldownEndTime = TimeSpan.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// The cooldown between generating anomalies.
|
||||
/// </summary>
|
||||
[DataField("cooldownLength"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public TimeSpan CooldownLength = TimeSpan.FromMinutes(5);
|
||||
|
||||
/// <summary>
|
||||
/// The material needed to generate an anomaly
|
||||
/// </summary>
|
||||
[DataField("requiredMaterial", customTypeSerializer: typeof(PrototypeIdSerializer<MaterialPrototype>)), ViewVariables(VVAccess.ReadWrite)]
|
||||
public string RequiredMaterial = "Plasma";
|
||||
|
||||
/// <summary>
|
||||
/// The amount of material needed to generate a single anomaly
|
||||
/// </summary>
|
||||
[DataField("materialPerAnomaly"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public int MaterialPerAnomaly = 1500; // a bit less than a stack of plasma
|
||||
|
||||
/// <summary>
|
||||
/// The random anomaly spawner entity
|
||||
/// </summary>
|
||||
[DataField("spawnerPrototype", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>)), ViewVariables(VVAccess.ReadWrite)]
|
||||
public string SpawnerPrototype = "RandomAnomalySpawner";
|
||||
}
|
||||
49
Content.Server/Anomaly/Components/AnomalyScannerComponent.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using System.Threading;
|
||||
using Robust.Shared.Audio;
|
||||
|
||||
namespace Content.Server.Anomaly.Components;
|
||||
|
||||
/// <summary>
|
||||
/// This is used for scanning anomalies and
|
||||
/// displaying information about them in the ui
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed class AnomalyScannerComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The anomaly that was last scanned by this scanner.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public EntityUid? ScannedAnomaly;
|
||||
|
||||
/// <summary>
|
||||
/// How long the scan takes
|
||||
/// </summary>
|
||||
[DataField("scanDoAfterDuration")]
|
||||
public float ScanDoAfterDuration = 5;
|
||||
|
||||
public CancellationTokenSource? TokenSource;
|
||||
|
||||
/// <summary>
|
||||
/// The sound plays when the scan finished
|
||||
/// </summary>
|
||||
[DataField("completeSound")]
|
||||
public SoundSpecifier? CompleteSound = new SoundPathSpecifier("/Audio/Items/beep.ogg");
|
||||
}
|
||||
|
||||
public sealed class AnomalyScanFinishedEvent : EntityEventArgs
|
||||
{
|
||||
public EntityUid Anomaly;
|
||||
|
||||
public EntityUid User;
|
||||
|
||||
public AnomalyScanFinishedEvent(EntityUid anomaly, EntityUid user)
|
||||
{
|
||||
Anomaly = anomaly;
|
||||
User = user;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class AnomalyScanCancelledEvent : EntityEventArgs
|
||||
{
|
||||
}
|
||||
40
Content.Server/Anomaly/Components/AnomalyVesselComponent.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using Content.Shared.Construction.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
namespace Content.Server.Anomaly.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Anomaly Vessels can have an anomaly "stored" in them
|
||||
/// by interacting on them with an anomaly scanner. Then,
|
||||
/// they generate points for the selected server based on
|
||||
/// the anomaly's stability and severity.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed class AnomalyVesselComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The anomaly that the vessel is storing.
|
||||
/// Can be null.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public EntityUid? Anomaly;
|
||||
|
||||
/// <summary>
|
||||
/// A multiplier applied to the amount of points generated.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float PointMultiplier = 1;
|
||||
|
||||
/// <summary>
|
||||
/// The machine part that affects the point multiplier of the vessel
|
||||
/// </summary>
|
||||
[DataField("machinePartPointModifier", customTypeSerializer: typeof(PrototypeIdSerializer<MachinePartPrototype>))]
|
||||
public string MachinePartPointModifier = "ScanningModule";
|
||||
|
||||
/// <summary>
|
||||
/// A value used to scale the point multiplier
|
||||
/// with the corresponding part rating.
|
||||
/// </summary>
|
||||
[DataField("partRatingPointModifier")]
|
||||
public float PartRatingPointModifier = 1.5f;
|
||||
}
|
||||
61
Content.Server/Anomaly/Effects/ElectricityAnomalySystem.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using Content.Server.Electrocution;
|
||||
using Content.Server.Lightning;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Shared.Anomaly.Components;
|
||||
using Content.Shared.Anomaly.Effects.Components;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Content.Shared.StatusEffect;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.Anomaly.Effects;
|
||||
|
||||
public sealed class ElectricityAnomalySystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly LightningSystem _lightning = default!;
|
||||
[Dependency] private readonly ElectrocutionSystem _electrocution = default!;
|
||||
[Dependency] private readonly EntityLookupSystem _lookup = default!;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Initialize()
|
||||
{
|
||||
SubscribeLocalEvent<ElectricityAnomalyComponent, AnomalyPulseEvent>(OnPulse);
|
||||
SubscribeLocalEvent<ElectricityAnomalyComponent, AnomalySupercriticalEvent>(OnSupercritical);
|
||||
}
|
||||
|
||||
private void OnPulse(EntityUid uid, ElectricityAnomalyComponent component, ref AnomalyPulseEvent args)
|
||||
{
|
||||
var range = component.MaxElectrocuteRange * args.Stabiltiy;
|
||||
var damage = (int) (component.MaxElectrocuteDamage * args.Severity);
|
||||
var duration = component.MaxElectrocuteDuration * args.Severity;
|
||||
|
||||
var xform = Transform(uid);
|
||||
foreach (var comp in _lookup.GetComponentsInRange<StatusEffectsComponent>(xform.MapPosition, range))
|
||||
{
|
||||
var ent = comp.Owner;
|
||||
|
||||
_electrocution.TryDoElectrocution(ent, uid, damage, duration, true, statusEffects: comp, ignoreInsulation: true);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSupercritical(EntityUid uid, ElectricityAnomalyComponent component, ref AnomalySupercriticalEvent args)
|
||||
{
|
||||
var poweredQuery = GetEntityQuery<ApcPowerReceiverComponent>();
|
||||
var mobQuery = GetEntityQuery<MobThresholdsComponent>();
|
||||
var validEnts = new HashSet<EntityUid>();
|
||||
foreach (var ent in _lookup.GetEntitiesInRange(uid, component.MaxElectrocuteRange * 2))
|
||||
{
|
||||
if (mobQuery.HasComponent(ent))
|
||||
validEnts.Add(ent);
|
||||
|
||||
if (_random.Prob(0.1f) && poweredQuery.HasComponent(ent))
|
||||
validEnts.Add(ent);
|
||||
}
|
||||
|
||||
// goodbye, sweet perf
|
||||
foreach (var ent in validEnts)
|
||||
{
|
||||
_lightning.ShootLightning(uid, ent);
|
||||
}
|
||||
}
|
||||
}
|
||||
39
Content.Server/Anomaly/Effects/GravityAnomalySystem.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using Content.Server.Singularity.Components;
|
||||
using Content.Shared.Anomaly.Components;
|
||||
using Content.Shared.Anomaly.Effects;
|
||||
using Content.Shared.Anomaly.Effects.Components;
|
||||
using Content.Shared.Radiation.Components;
|
||||
|
||||
namespace Content.Server.Anomaly.Effects;
|
||||
|
||||
/// <summary>
|
||||
/// This handles logic and events relating to <see cref="GravityAnomalyComponent"/> and <seealso cref="AnomalySystem"/>
|
||||
/// </summary>
|
||||
public sealed class GravityAnomalySystem : SharedGravityAnomalySystem
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<GravityAnomalyComponent, AnomalySeverityChangedEvent>(OnSeverityChanged);
|
||||
SubscribeLocalEvent<GravityAnomalyComponent, AnomalyStabilityChangedEvent>(OnStabilityChanged);
|
||||
}
|
||||
|
||||
private void OnSeverityChanged(EntityUid uid, GravityAnomalyComponent component, ref AnomalySeverityChangedEvent args)
|
||||
{
|
||||
if (TryComp<RadiationSourceComponent>(uid, out var radSource))
|
||||
radSource.Intensity = component.MaxRadiationIntensity * args.Severity;
|
||||
|
||||
if (!TryComp<GravityWellComponent>(uid, out var gravityWell))
|
||||
return;
|
||||
var accel = (component.MaxAccel - component.MinAccel) * args.Severity + component.MinAccel;
|
||||
gravityWell.BaseRadialAcceleration = accel;
|
||||
gravityWell.BaseTangentialAcceleration = accel * 0.2f;
|
||||
}
|
||||
|
||||
private void OnStabilityChanged(EntityUid uid, GravityAnomalyComponent component, ref AnomalyStabilityChangedEvent args)
|
||||
{
|
||||
if (TryComp<GravityWellComponent>(uid, out var gravityWell))
|
||||
gravityWell.MaxRange = component.MaxGravityWellRange * args.Stability;
|
||||
}
|
||||
}
|
||||
100
Content.Server/Anomaly/Effects/PyroclasticAnomalySystem.cs
Normal file
@@ -0,0 +1,100 @@
|
||||
using Content.Server.Atmos.Components;
|
||||
using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Server.Interaction;
|
||||
using Content.Shared.Anomaly.Components;
|
||||
using Content.Shared.Anomaly.Effects.Components;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Server.Anomaly.Effects;
|
||||
|
||||
/// <summary>
|
||||
/// This handles <see cref="PyroclasticAnomalyComponent"/> and the events from <seealso cref="AnomalySystem"/>
|
||||
/// </summary>
|
||||
public sealed class PyroclasticAnomalySystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly AtmosphereSystem _atmosphere = default!;
|
||||
[Dependency] private readonly EntityLookupSystem _lookup = default!;
|
||||
[Dependency] private readonly FlammableSystem _flammable = default!;
|
||||
[Dependency] private readonly InteractionSystem _interaction = default!;
|
||||
[Dependency] private readonly TransformSystem _xform = default!;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Initialize()
|
||||
{
|
||||
SubscribeLocalEvent<PyroclasticAnomalyComponent, AnomalyPulseEvent>(OnPulse);
|
||||
SubscribeLocalEvent<PyroclasticAnomalyComponent, AnomalySupercriticalEvent>(OnSupercritical);
|
||||
}
|
||||
|
||||
private void OnPulse(EntityUid uid, PyroclasticAnomalyComponent component, ref AnomalyPulseEvent args)
|
||||
{
|
||||
var xform = Transform(uid);
|
||||
var ignitionRadius = component.MaximumIgnitionRadius * args.Stabiltiy;
|
||||
IgniteNearby(xform.Coordinates, args.Severity, ignitionRadius);
|
||||
}
|
||||
|
||||
private void OnSupercritical(EntityUid uid, PyroclasticAnomalyComponent component, ref AnomalySupercriticalEvent args)
|
||||
{
|
||||
var xform = Transform(uid);
|
||||
var grid = xform.GridUid;
|
||||
var map = xform.MapUid;
|
||||
|
||||
var indices = _xform.GetGridOrMapTilePosition(uid, xform);
|
||||
var mixture = _atmosphere.GetTileMixture(grid, map, indices, true);
|
||||
|
||||
if (mixture == null)
|
||||
return;
|
||||
mixture.AdjustMoles(component.SupercriticalGas, component.SupercriticalMoleAmount);
|
||||
if (grid is { })
|
||||
{
|
||||
foreach (var ind in _atmosphere.GetAdjacentTiles(grid.Value, indices))
|
||||
{
|
||||
var mix = _atmosphere.GetTileMixture(grid, map, indices, true);
|
||||
if (mix is not {})
|
||||
continue;
|
||||
mix.AdjustMoles(component.SupercriticalGas, component.SupercriticalMoleAmount);
|
||||
mix.Temperature += component.HotspotExposeTemperature;
|
||||
_atmosphere.HotspotExpose(grid.Value, indices, component.HotspotExposeTemperature, mix?.Volume ?? component.SupercriticalMoleAmount, true);
|
||||
}
|
||||
}
|
||||
IgniteNearby(xform.Coordinates, 1, component.MaximumIgnitionRadius * 2);
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
foreach (var (pyro, anom, xform) in EntityQuery<PyroclasticAnomalyComponent, AnomalyComponent, TransformComponent>())
|
||||
{
|
||||
var ent = pyro.Owner;
|
||||
|
||||
var grid = xform.GridUid;
|
||||
var map = xform.MapUid;
|
||||
var indices = _xform.GetGridOrMapTilePosition(ent, xform);
|
||||
var mixture = _atmosphere.GetTileMixture(grid, map, indices, true);
|
||||
if (mixture is { })
|
||||
{
|
||||
mixture.Temperature += pyro.HeatPerSecond * anom.Severity * frameTime;
|
||||
}
|
||||
|
||||
if (grid != null && anom.Severity > pyro.AnomalyHotspotThreshold)
|
||||
{
|
||||
_atmosphere.HotspotExpose(grid.Value, indices, pyro.HotspotExposeTemperature, pyro.HotspotExposeVolume, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void IgniteNearby(EntityCoordinates coordinates, float severity, float radius)
|
||||
{
|
||||
foreach (var flammable in _lookup.GetComponentsInRange<FlammableComponent>(coordinates, radius))
|
||||
{
|
||||
var ent = flammable.Owner;
|
||||
if (!_interaction.InRangeUnobstructed(coordinates.ToMap(EntityManager), ent, -1))
|
||||
continue;
|
||||
|
||||
var stackAmount = 1 + (int) (severity / 0.25f);
|
||||
_flammable.AdjustFireStacks(ent, stackAmount, flammable);
|
||||
_flammable.Ignite(ent, flammable);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,11 @@
|
||||
using Content.Server.Beam.Components;
|
||||
using Content.Server.Lightning;
|
||||
using Content.Shared.Beam;
|
||||
using Content.Shared.Beam.Components;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Physics;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Collision.Shapes;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Physics.Dynamics;
|
||||
using Robust.Shared.Physics.Systems;
|
||||
|
||||
namespace Content.Server.Beam;
|
||||
@@ -17,8 +14,8 @@ public sealed class BeamSystem : SharedBeamSystem
|
||||
{
|
||||
[Dependency] private readonly FixtureSystem _fixture = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly SharedBroadphaseSystem _broadphase = default!;
|
||||
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -79,8 +76,8 @@ public sealed class BeamSystem : SharedBeamSystem
|
||||
var shape = new EdgeShape(distanceCorrection, new Vector2(0,0));
|
||||
var distanceLength = distanceCorrection.Length;
|
||||
|
||||
if (TryComp<PhysicsComponent>(ent, out var physics) && TryComp<BeamComponent>(ent, out var beam))
|
||||
{
|
||||
if (!TryComp<PhysicsComponent>(ent, out var physics) || !TryComp<BeamComponent>(ent, out var beam))
|
||||
return;
|
||||
FixturesComponent? manager = null;
|
||||
_fixture.TryCreateFixture(
|
||||
ent,
|
||||
@@ -94,6 +91,7 @@ public sealed class BeamSystem : SharedBeamSystem
|
||||
|
||||
_physics.SetBodyType(ent, BodyType.Dynamic, manager: manager, body: physics);
|
||||
_physics.SetCanCollide(ent, true, manager: manager, body: physics);
|
||||
_broadphase.RegenerateContacts(physics, manager);
|
||||
|
||||
var beamVisualizerEvent = new BeamVisualizerEvent(ent, distanceLength, userAngle, bodyState, shader);
|
||||
RaiseNetworkEvent(beamVisualizerEvent);
|
||||
@@ -113,7 +111,7 @@ public sealed class BeamSystem : SharedBeamSystem
|
||||
}
|
||||
|
||||
//Create the rest of the beam, sprites handled through the BeamVisualizerEvent
|
||||
for (int i = 0; i < distanceLength-1; i++)
|
||||
for (var i = 0; i < distanceLength-1; i++)
|
||||
{
|
||||
beamSpawnPos = beamSpawnPos.Offset(calculatedDistance.Normalized);
|
||||
var newEnt = Spawn(prototype, beamSpawnPos);
|
||||
@@ -125,7 +123,6 @@ public sealed class BeamSystem : SharedBeamSystem
|
||||
var beamFiredEvent = new BeamFiredEvent(ent);
|
||||
RaiseLocalEvent(beam.VirtualBeamController.Value, beamFiredEvent);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called where you want an entity to create a beam from one target to another.
|
||||
|
||||
@@ -7,16 +7,19 @@ using Content.Server.Projectiles;
|
||||
using Content.Server.Storage.Components;
|
||||
using Content.Server.Weapons.Ranged.Systems;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Projectiles;
|
||||
using Content.Shared.Singularity.Components;
|
||||
using Content.Shared.Singularity.EntitySystems;
|
||||
using Content.Shared.Verbs;
|
||||
using Content.Shared.Weapons.Ranged.Components;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Utility;
|
||||
using Timer = Robust.Shared.Timing.Timer;
|
||||
@@ -27,6 +30,7 @@ namespace Content.Server.Singularity.EntitySystems
|
||||
public sealed class EmitterSystem : SharedEmitterSystem
|
||||
{
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
@@ -38,14 +42,28 @@ namespace Content.Server.Singularity.EntitySystems
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<EmitterComponent, PowerConsumerReceivedChanged>(ReceivedChanged);
|
||||
SubscribeLocalEvent<EmitterComponent, PowerChangedEvent>(OnApcChanged);
|
||||
SubscribeLocalEvent<EmitterComponent, InteractHandEvent>(OnInteractHand);
|
||||
SubscribeLocalEvent<EmitterComponent, GetVerbsEvent<Verb>>(OnGetVerb);
|
||||
SubscribeLocalEvent<EmitterComponent, ExaminedEvent>(OnExamined);
|
||||
SubscribeLocalEvent<EmitterComponent, RefreshPartsEvent>(OnRefreshParts);
|
||||
SubscribeLocalEvent<EmitterComponent, UpgradeExamineEvent>(OnUpgradeExamine);
|
||||
SubscribeLocalEvent<EmitterComponent, AnchorStateChangedEvent>(OnAnchorStateChanged);
|
||||
}
|
||||
|
||||
private void OnAnchorStateChanged(EntityUid uid, EmitterComponent component, ref AnchorStateChangedEvent args)
|
||||
{
|
||||
if (args.Anchored)
|
||||
return;
|
||||
|
||||
SwitchOff(component);
|
||||
}
|
||||
|
||||
private void OnInteractHand(EntityUid uid, EmitterComponent component, InteractHandEvent args)
|
||||
{
|
||||
args.Handled = true;
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
if (EntityManager.TryGetComponent(uid, out LockComponent? lockComp) && lockComp.Locked)
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("comp-emitter-access-locked",
|
||||
@@ -71,6 +89,7 @@ namespace Content.Server.Singularity.EntitySystems
|
||||
_adminLogger.Add(LogType.Emitter,
|
||||
component.IsOn ? LogImpact.Medium : LogImpact.High,
|
||||
$"{ToPrettyString(args.User):player} toggled {ToPrettyString(uid):emitter}");
|
||||
args.Handled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -79,6 +98,47 @@ namespace Content.Server.Singularity.EntitySystems
|
||||
}
|
||||
}
|
||||
|
||||
private void OnGetVerb(EntityUid uid, EmitterComponent component, GetVerbsEvent<Verb> args)
|
||||
{
|
||||
if (!args.CanAccess || !args.CanInteract || args.Hands == null)
|
||||
return;
|
||||
|
||||
if (TryComp<LockComponent>(uid, out var lockComp) && lockComp.Locked)
|
||||
return;
|
||||
|
||||
if (component.SelectableTypes.Count < 2)
|
||||
return;
|
||||
|
||||
foreach (var type in component.SelectableTypes)
|
||||
{
|
||||
var proto = _prototype.Index<EntityPrototype>(type);
|
||||
|
||||
var v = new Verb
|
||||
{
|
||||
Priority = 1,
|
||||
Category = VerbCategory.SelectType,
|
||||
Text = proto.Name,
|
||||
Disabled = type == component.BoltType,
|
||||
Impact = LogImpact.Medium,
|
||||
DoContactInteraction = true,
|
||||
Act = () =>
|
||||
{
|
||||
component.BoltType = type;
|
||||
_popup.PopupEntity(Loc.GetString("emitter-component-type-set", ("type", proto.Name)), uid);
|
||||
}
|
||||
};
|
||||
args.Verbs.Add(v);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnExamined(EntityUid uid, EmitterComponent component, ExaminedEvent args)
|
||||
{
|
||||
if (component.SelectableTypes.Count < 2)
|
||||
return;
|
||||
var proto = _prototype.Index<EntityPrototype>(component.BoltType);
|
||||
args.Message.AddText(Loc.GetString("emitter-component-current-type", ("type", proto.Name)));
|
||||
}
|
||||
|
||||
private void ReceivedChanged(
|
||||
EntityUid uid,
|
||||
EmitterComponent component,
|
||||
@@ -99,6 +159,23 @@ namespace Content.Server.Singularity.EntitySystems
|
||||
}
|
||||
}
|
||||
|
||||
private void OnApcChanged(EntityUid uid, EmitterComponent component, ref PowerChangedEvent args)
|
||||
{
|
||||
if (!component.IsOn)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!args.Powered)
|
||||
{
|
||||
PowerOff(component);
|
||||
}
|
||||
else
|
||||
{
|
||||
PowerOn(component);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnRefreshParts(EntityUid uid, EmitterComponent component, RefreshPartsEvent args)
|
||||
{
|
||||
var powerUseRating = args.PartRatings[component.MachinePartPowerUse];
|
||||
@@ -122,7 +199,9 @@ namespace Content.Server.Singularity.EntitySystems
|
||||
{
|
||||
component.IsOn = false;
|
||||
if (TryComp<PowerConsumerComponent>(component.Owner, out var powerConsumer))
|
||||
powerConsumer.DrawRate = 0;
|
||||
powerConsumer.DrawRate = 1; // this needs to be not 0 so that the visuals still work.
|
||||
if (TryComp<ApcPowerReceiverComponent>(component.Owner, out var apcReceiever))
|
||||
apcReceiever.Load = 1;
|
||||
PowerOff(component);
|
||||
UpdateAppearance(component);
|
||||
}
|
||||
@@ -132,6 +211,11 @@ namespace Content.Server.Singularity.EntitySystems
|
||||
component.IsOn = true;
|
||||
if (TryComp<PowerConsumerComponent>(component.Owner, out var powerConsumer))
|
||||
powerConsumer.DrawRate = component.PowerUseActive;
|
||||
if (TryComp<ApcPowerReceiverComponent>(component.Owner, out var apcReceiever))
|
||||
{
|
||||
apcReceiever.Load = component.PowerUseActive;
|
||||
PowerOn(component);
|
||||
}
|
||||
// Do not directly PowerOn().
|
||||
// OnReceivedPowerChanged will get fired due to DrawRate change which will turn it on.
|
||||
UpdateAppearance(component);
|
||||
@@ -179,9 +263,6 @@ namespace Content.Server.Singularity.EntitySystems
|
||||
// and thus not firing
|
||||
DebugTools.Assert(component.IsPowered);
|
||||
DebugTools.Assert(component.IsOn);
|
||||
DebugTools.Assert(TryComp<PowerConsumerComponent>(component.Owner, out var powerConsumer) &&
|
||||
(powerConsumer.DrawRate <= powerConsumer.ReceivedPower ||
|
||||
MathHelper.CloseTo(powerConsumer.DrawRate, powerConsumer.ReceivedPower, 0.0001f)));
|
||||
|
||||
Fire(component);
|
||||
|
||||
|
||||
@@ -81,4 +81,5 @@ public enum LogType
|
||||
Stamina = 76,
|
||||
EntitySpawn = 77,
|
||||
AdminMessage = 78,
|
||||
Anomaly = 79
|
||||
}
|
||||
|
||||
268
Content.Shared/Anomaly/Components/AnomalyComponent.cs
Normal file
@@ -0,0 +1,268 @@
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
|
||||
namespace Content.Shared.Anomaly.Components;
|
||||
|
||||
/// <summary>
|
||||
/// This is used for tracking the general behavior of anomalies.
|
||||
/// This doesn't contain the specific implementations for what
|
||||
/// they do, just the generic behaviors associated with them.
|
||||
///
|
||||
/// Anomalies and their related components were designed here: https://hackmd.io/@ss14-design/r1sQbkJOs
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
public sealed class AnomalyComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// How likely an anomaly is to grow more dangerous. Moves both up and down.
|
||||
/// Ranges from 0 to 1.
|
||||
/// Values less than 0.5 indicate stability, whereas values greater
|
||||
/// than 0.5 indicate instability, which causes increases in severity.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Note that this doesn't refer to stability as a percentage: This is an arbitrary
|
||||
/// value that only matters in relation to the <see cref="GrowthThreshold"/> and <see cref="DecayThreshold"/>
|
||||
/// </remarks>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float Stability = 0f;
|
||||
|
||||
/// <summary>
|
||||
/// How severe the effects of an anomaly are. Moves only upwards.
|
||||
/// Ranges from 0 to 1.
|
||||
/// A value of 0 indicates effects of extrememly minimal severity, whereas greater
|
||||
/// values indicate effects of linearly increasing severity.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Wacky-Stability scale lives on in my heart. - emo
|
||||
/// </remarks>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float Severity = 0f;
|
||||
|
||||
#region Health
|
||||
/// <summary>
|
||||
/// The internal "health" of an anomaly.
|
||||
/// Ranges from 0 to 1.
|
||||
/// When the health of an anomaly reaches 0, it is destroyed without ever
|
||||
/// reaching a supercritical point.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float Health = 1f;
|
||||
|
||||
/// <summary>
|
||||
/// If the <see cref="Stability"/> of the anomaly exceeds this value, it
|
||||
/// becomes too unstable to support itself and starts decreasing in <see cref="Health"/>.
|
||||
/// </summary>
|
||||
[DataField("decayhreshold"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public float DecayThreshold = 0.15f;
|
||||
|
||||
/// <summary>
|
||||
/// The amount of health lost when the stability is below the <see cref="DecayThreshold"/>
|
||||
/// </summary>
|
||||
[DataField("healthChangePerSecond"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public float HealthChangePerSecond = -0.05f;
|
||||
#endregion
|
||||
|
||||
#region Growth
|
||||
/// <summary>
|
||||
/// If the <see cref="Stability"/> of the anomaly exceeds this value, it
|
||||
/// becomes unstable and starts increasing in <see cref="Severity"/>.
|
||||
/// </summary>
|
||||
[DataField("growthThreshold"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public float GrowthThreshold = 0.5f;
|
||||
|
||||
/// <summary>
|
||||
/// A coefficient used for calculating the increase in severity when above the GrowthThreshold
|
||||
/// </summary>
|
||||
[DataField("severityGrowthCoefficient"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public float SeverityGrowthCoefficient = 0.07f;
|
||||
#endregion
|
||||
|
||||
#region Pulse
|
||||
/// <summary>
|
||||
/// The time at which the next artifact pulse will occur.
|
||||
/// </summary>
|
||||
[DataField("nextPulseTime", customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)]
|
||||
public TimeSpan NextPulseTime = TimeSpan.MaxValue;
|
||||
|
||||
/// <summary>
|
||||
/// The minimum interval between pulses.
|
||||
/// </summary>
|
||||
[DataField("minPulseLength")]
|
||||
public TimeSpan MinPulseLength = TimeSpan.FromMinutes(1);
|
||||
|
||||
/// <summary>
|
||||
/// The maximum interval between pulses.
|
||||
/// </summary>
|
||||
[DataField("maxPulseLength")]
|
||||
public TimeSpan MaxPulseLength = TimeSpan.FromMinutes(2);
|
||||
|
||||
/// <summary>
|
||||
/// A percentage by which the length of a pulse might vary.
|
||||
/// </summary>
|
||||
[DataField("pulseVariation")]
|
||||
public float PulseVariation = .1f;
|
||||
|
||||
/// <summary>
|
||||
/// The sound played when an anomaly pulses
|
||||
/// </summary>
|
||||
[DataField("pulseSound")]
|
||||
public SoundSpecifier? PulseSound = new SoundCollectionSpecifier("RadiationPulse");
|
||||
|
||||
/// <summary>
|
||||
/// The sound plays when an anomaly goes supercritical
|
||||
/// </summary>
|
||||
[DataField("supercriticalSound")]
|
||||
public SoundSpecifier? SupercriticalSound = new SoundCollectionSpecifier("explosion");
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// The range of initial values for stability
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// +/- 0.2 from perfect stability (0.5)
|
||||
/// </remarks>
|
||||
[DataField("initialStabilityRange")]
|
||||
public (float, float) InitialStabilityRange = (0.4f, 0.6f);
|
||||
|
||||
/// <summary>
|
||||
/// The range of initial values for severity
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Between 0 and 0.5, which should be all mild effects
|
||||
/// </remarks>
|
||||
[DataField("initialSeverityRange")]
|
||||
public (float, float) InitialSeverityRange = (0.1f, 0.5f);
|
||||
|
||||
/// <summary>
|
||||
/// The particle type that increases the severity of the anomaly.
|
||||
/// </summary>
|
||||
[DataField("severityParticleType")]
|
||||
public AnomalousParticleType SeverityParticleType;
|
||||
|
||||
/// <summary>
|
||||
/// The amount that the <see cref="Severity"/> increases by when hit
|
||||
/// of an anomalous particle of <seealso cref="SeverityParticleType"/>.
|
||||
/// </summary>
|
||||
[DataField("severityPerSeverityHit")]
|
||||
public float SeverityPerSeverityHit = 0.025f;
|
||||
|
||||
/// <summary>
|
||||
/// The particle type that destabilizes the anomaly.
|
||||
/// </summary>
|
||||
[DataField("destabilizingParticleType")]
|
||||
public AnomalousParticleType DestabilizingParticleType;
|
||||
|
||||
/// <summary>
|
||||
/// The amount that the <see cref="Stability"/> increases by when hit
|
||||
/// of an anomalous particle of <seealso cref="DestabilizingParticleType"/>.
|
||||
/// </summary>
|
||||
[DataField("stabilityPerDestabilizingHit")]
|
||||
public float StabilityPerDestabilizingHit = 0.04f;
|
||||
|
||||
/// <summary>
|
||||
/// The particle type that weakens the anomalys health.
|
||||
/// </summary>
|
||||
[DataField("weakeningParticleType")]
|
||||
public AnomalousParticleType WeakeningParticleType;
|
||||
|
||||
/// <summary>
|
||||
/// The amount that the <see cref="Stability"/> increases by when hit
|
||||
/// of an anomalous particle of <seealso cref="DestabilizingParticleType"/>.
|
||||
/// </summary>
|
||||
[DataField("healthPerWeakeningeHit")]
|
||||
public float HealthPerWeakeningeHit = -0.05f;
|
||||
|
||||
/// <summary>
|
||||
/// The amount that the <see cref="Stability"/> increases by when hit
|
||||
/// of an anomalous particle of <seealso cref="DestabilizingParticleType"/>.
|
||||
/// </summary>
|
||||
[DataField("stabilityPerWeakeningeHit")]
|
||||
public float StabilityPerWeakeningeHit = -0.02f;
|
||||
|
||||
#region Points and Vessels
|
||||
/// <summary>
|
||||
/// The vessel that the anomaly is connceted to. Stored so that multiple
|
||||
/// vessels cannot connect to the same anomaly.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public EntityUid? ConnectedVessel;
|
||||
|
||||
/// <summary>
|
||||
/// The minimum amount of research points generated per second
|
||||
/// </summary>
|
||||
[DataField("minPointsPerSecond")]
|
||||
public int MinPointsPerSecond;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum amount of research points generated per second
|
||||
/// This doesn't include the point bonus for being unstable.
|
||||
/// </summary>
|
||||
[DataField("maxPointsPerSecond")]
|
||||
public int MaxPointsPerSecond = 100;
|
||||
#endregion
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class AnomalyComponentState : ComponentState
|
||||
{
|
||||
public float Severity;
|
||||
public float Stability;
|
||||
public float Health;
|
||||
public TimeSpan NextPulseTime;
|
||||
|
||||
public AnomalyComponentState(float severity, float stability, float health, TimeSpan nextPulseTime)
|
||||
{
|
||||
Severity = severity;
|
||||
Stability = stability;
|
||||
Health = health;
|
||||
NextPulseTime = nextPulseTime;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event raised at regular intervals on an anomaly to do whatever its effect is.
|
||||
/// </summary>
|
||||
/// <param name="Stabiltiy"></param>
|
||||
/// <param name="Severity"></param>
|
||||
[ByRefEvent]
|
||||
public readonly record struct AnomalyPulseEvent(float Stabiltiy, float Severity)
|
||||
{
|
||||
public readonly float Stabiltiy = Stabiltiy;
|
||||
public readonly float Severity = Severity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event raised on an anomaly when it reaches a supercritical point.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public readonly record struct AnomalySupercriticalEvent;
|
||||
|
||||
/// <summary>
|
||||
/// Event broadcast after an anomaly goes supercritical
|
||||
/// </summary>
|
||||
/// <param name="Anomaly">The anomaly being shut down.</param>
|
||||
/// <param name="Supercritical">Whether or not the anomaly shut down passively or via a supercritical event.</param>
|
||||
[ByRefEvent]
|
||||
public readonly record struct AnomalyShutdownEvent(EntityUid Anomaly, bool Supercritical);
|
||||
|
||||
/// <summary>
|
||||
/// Event broadcast when an anomaly's severity is changed.
|
||||
/// </summary>
|
||||
/// <param name="Anomaly">The anomaly being changed</param>
|
||||
[ByRefEvent]
|
||||
public readonly record struct AnomalySeverityChangedEvent(EntityUid Anomaly, float Severity);
|
||||
|
||||
/// <summary>
|
||||
/// Event broadcast when an anomaly's stability is changed.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public readonly record struct AnomalyStabilityChangedEvent(EntityUid Anomaly, float Stability);
|
||||
|
||||
/// <summary>
|
||||
/// Event broadcast when an anomaly's health is changed.
|
||||
/// </summary>
|
||||
/// <param name="Anomaly">The anomaly being changed</param>
|
||||
[ByRefEvent]
|
||||
public readonly record struct AnomalyHealthChangedEvent(EntityUid Anomaly, float Health);
|
||||
22
Content.Shared/Anomaly/Components/AnomalyPulsingComponent.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
|
||||
namespace Content.Shared.Anomaly.Components;
|
||||
|
||||
/// <summary>
|
||||
/// This component tracks anomalies that are currently pulsing
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed class AnomalyPulsingComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The time at which the pulse will be over.
|
||||
/// </summary>
|
||||
[DataField("endTime", customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)]
|
||||
public TimeSpan EndTime = TimeSpan.MaxValue;
|
||||
|
||||
/// <summary>
|
||||
/// How long the pulse visual lasts
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public TimeSpan PulseDuration = TimeSpan.FromSeconds(5);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
|
||||
namespace Content.Shared.Anomaly.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Tracks anomalies going supercritical
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
public sealed class AnomalySupercriticalComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The time when the supercritical animation ends and it does whatever effect.
|
||||
/// </summary>
|
||||
[DataField("endTime", customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)]
|
||||
public TimeSpan EndTime = TimeSpan.MaxValue;
|
||||
|
||||
/// <summary>
|
||||
/// The length of the animation before it goes supercritical.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public TimeSpan SupercriticalDuration = TimeSpan.FromSeconds(10);
|
||||
|
||||
/// <summary>
|
||||
/// The maximum size the anomaly scales to while going supercritical
|
||||
/// </summary>
|
||||
[DataField("maxScaleAmount")]
|
||||
public float MaxScaleAmount = 3;
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class AnomalySupercriticalComponentState : ComponentState
|
||||
{
|
||||
public TimeSpan EndTime;
|
||||
public TimeSpan Duration;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace Content.Shared.Anomaly.Effects.Components;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed class ElectricityAnomalyComponent : Component
|
||||
{
|
||||
[DataField("maxElectrocutionRange"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public float MaxElectrocuteRange = 6f;
|
||||
|
||||
[DataField("maxElectrocuteDamage"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public float MaxElectrocuteDamage = 20f;
|
||||
|
||||
[DataField("maxElectrocuteDuration"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public TimeSpan MaxElectrocuteDuration = TimeSpan.FromSeconds(8);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
namespace Content.Shared.Anomaly.Effects.Components;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed class GravityAnomalyComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The maximumum size the GravityWellComponent MaxRange can be.
|
||||
/// Is scaled linearly with stability.
|
||||
/// </summary>
|
||||
[DataField("maxGravityWellRange"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public float MaxGravityWellRange = 8f;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum distance from which the anomaly
|
||||
/// can throw you via a pulse.
|
||||
/// </summary>
|
||||
[DataField("maxThrowRange"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public float MaxThrowRange = 5f;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum strength the anomaly
|
||||
/// can throw you via a pulse
|
||||
/// </summary>
|
||||
[DataField("maxThrowStrength"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public float MaxThrowStrength = 10;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum Intensity of the RadiationSourceComponent.
|
||||
/// Is scaled linearly with stability.
|
||||
/// </summary>
|
||||
[DataField("maxRadiationIntensity"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public float MaxRadiationIntensity = 3f;
|
||||
|
||||
/// <summary>
|
||||
/// The minimum acceleration value for GravityWellComponent
|
||||
/// Is scaled linearly with stability.
|
||||
/// </summary>
|
||||
[DataField("minAccel"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public float MinAccel = 1f;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum acceleration value for GravityWellComponent
|
||||
/// Is scaled linearly with stability.
|
||||
/// </summary>
|
||||
[DataField("maxAccel"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public float MaxAccel = 5f;
|
||||
|
||||
/// <summary>
|
||||
/// The range around the anomaly that will be spaced on supercritical.
|
||||
/// </summary>
|
||||
[DataField("spaceRange"), ViewVariables(VVAccess.ReadWrite)]
|
||||
public float SpaceRange = 3f;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using Content.Shared.Atmos;
|
||||
|
||||
namespace Content.Shared.Anomaly.Effects.Components;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed class PyroclasticAnomalyComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The MAXIMUM amount of heat released per second.
|
||||
/// This is scaled linearly with the Severity of the anomaly.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// I have no clue if this is balanced.
|
||||
/// </remarks>
|
||||
[DataField("heatPerSecond")]
|
||||
public float HeatPerSecond = 50;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum distance from which you can be ignited by the anomaly.
|
||||
/// </summary>
|
||||
[DataField("maximumIgnitionRadius")]
|
||||
public float MaximumIgnitionRadius = 8f;
|
||||
|
||||
/// <summary>
|
||||
/// The minimum amount of severity required
|
||||
/// before the anomaly becomes a hotspot.
|
||||
/// </summary>
|
||||
[DataField("anomalyHotspotThreshold")]
|
||||
public float AnomalyHotspotThreshold = 0.6f;
|
||||
|
||||
/// <summary>
|
||||
/// The temperature of the hotspot where the anomaly is
|
||||
/// </summary>
|
||||
[DataField("hotspotExposeTemperature")]
|
||||
public float HotspotExposeTemperature = 1000;
|
||||
|
||||
/// <summary>
|
||||
/// The volume of the hotspot where the anomaly is.
|
||||
/// </summary>
|
||||
[DataField("hotspotExposeVolume")]
|
||||
public float HotspotExposeVolume = 50;
|
||||
|
||||
/// <summary>
|
||||
/// Gas released when the anomaly goes supercritical.
|
||||
/// </summary>
|
||||
[DataField("supercriticalGas")]
|
||||
public Gas SupercriticalGas = Gas.Plasma;
|
||||
|
||||
/// <summary>
|
||||
/// The amount of gas released when the anomaly goes supercritical
|
||||
/// </summary>
|
||||
[DataField("supercriticalMoleAmount")]
|
||||
public float SupercriticalMoleAmount = 50f;
|
||||
}
|
||||
65
Content.Shared/Anomaly/Effects/SharedGravityAnomalySystem.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
using System.Linq;
|
||||
using Content.Shared.Anomaly.Components;
|
||||
using Content.Shared.Anomaly.Effects.Components;
|
||||
using Content.Shared.Construction.Components;
|
||||
using Content.Shared.Construction.EntitySystems;
|
||||
using Content.Shared.Throwing;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Shared.Anomaly.Effects;
|
||||
|
||||
public abstract class SharedGravityAnomalySystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IMapManager _map = default!;
|
||||
[Dependency] private readonly EntityLookupSystem _lookup = default!;
|
||||
[Dependency] private readonly SharedAnchorableSystem _anchorable = default!;
|
||||
[Dependency] private readonly ThrowingSystem _throwing = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _xform = default!;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Initialize()
|
||||
{
|
||||
SubscribeLocalEvent<GravityAnomalyComponent, AnomalyPulseEvent>(OnAnomalyPulse);
|
||||
SubscribeLocalEvent<GravityAnomalyComponent, AnomalySupercriticalEvent>(OnSupercritical);
|
||||
}
|
||||
|
||||
private void OnAnomalyPulse(EntityUid uid, GravityAnomalyComponent component, ref AnomalyPulseEvent args)
|
||||
{
|
||||
var xform = Transform(uid);
|
||||
var range = component.MaxThrowRange * args.Severity;
|
||||
var strength = component.MaxThrowStrength * args.Severity;
|
||||
var lookup = _lookup.GetEntitiesInRange(uid, range, LookupFlags.Dynamic | LookupFlags.Sundries);
|
||||
foreach (var ent in lookup)
|
||||
{
|
||||
var tempXform = Transform(ent);
|
||||
|
||||
var foo = tempXform.MapPosition.Position - xform.MapPosition.Position;
|
||||
_throwing.TryThrow(ent, foo.Normalized * 10, strength, uid, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSupercritical(EntityUid uid, GravityAnomalyComponent component, ref AnomalySupercriticalEvent args)
|
||||
{
|
||||
var xform = Transform(uid);
|
||||
if (!_map.TryGetGrid(xform.GridUid, out var grid))
|
||||
return;
|
||||
|
||||
var worldPos = _xform.GetWorldPosition(xform);
|
||||
var tileref = grid.GetTilesIntersecting(new Circle(worldPos, component.SpaceRange)).ToArray();
|
||||
var tiles = tileref.Select(t => (t.GridIndices, Tile.Empty)).ToList();
|
||||
grid.SetTiles(tiles);
|
||||
|
||||
var range = component.MaxThrowRange * 2;
|
||||
var strength = component.MaxThrowStrength * 2;
|
||||
var lookup = _lookup.GetEntitiesInRange(uid, range, LookupFlags.Dynamic | LookupFlags.Sundries);
|
||||
foreach (var ent in lookup)
|
||||
{
|
||||
var tempXform = Transform(ent);
|
||||
|
||||
var foo = tempXform.MapPosition.Position - xform.MapPosition.Position;
|
||||
Logger.Debug($"{ToPrettyString(ent)}: {foo}: {foo.Normalized}: {foo.Normalized * 10}");
|
||||
_throwing.TryThrow(ent, foo * 5, strength, uid, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
97
Content.Shared/Anomaly/SharedAnomaly.cs
Normal file
@@ -0,0 +1,97 @@
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Shared.Anomaly;
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public enum AnomalyVisuals : byte
|
||||
{
|
||||
IsPulsing,
|
||||
Supercritical
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public enum AnomalyVisualLayers : byte
|
||||
{
|
||||
Base,
|
||||
Animated
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The types of anomalous particles used
|
||||
/// for interfacing with anomalies.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The only thought behind these names is that
|
||||
/// they're a continuation of radioactive particles.
|
||||
/// Yes i know detla+ waves exist, but they're not
|
||||
/// common enough for me to care.
|
||||
/// </remarks>
|
||||
[Serializable, NetSerializable]
|
||||
public enum AnomalousParticleType : byte
|
||||
{
|
||||
Delta,
|
||||
Epsilon,
|
||||
Zeta
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public enum AnomalyVesselVisuals : byte
|
||||
{
|
||||
HasAnomaly
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public enum AnomalyVesselVisualLayers : byte
|
||||
{
|
||||
Base
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public enum AnomalyScannerUiKey : byte
|
||||
{
|
||||
Key
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class AnomalyScannerUserInterfaceState : BoundUserInterfaceState
|
||||
{
|
||||
public FormattedMessage Message;
|
||||
|
||||
public TimeSpan? NextPulseTime;
|
||||
|
||||
public AnomalyScannerUserInterfaceState(FormattedMessage message, TimeSpan? nextPulseTime)
|
||||
{
|
||||
Message = message;
|
||||
NextPulseTime = nextPulseTime;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public enum AnomalyGeneratorUiKey : byte
|
||||
{
|
||||
Key
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class AnomalyGeneratorUserInterfaceState : BoundUserInterfaceState
|
||||
{
|
||||
public TimeSpan CooldownEndTime;
|
||||
|
||||
public int FuelAmount;
|
||||
|
||||
public int FuelCost;
|
||||
|
||||
public AnomalyGeneratorUserInterfaceState(TimeSpan cooldownEndTime, int fuelAmount, int fuelCost)
|
||||
{
|
||||
CooldownEndTime = cooldownEndTime;
|
||||
FuelAmount = fuelAmount;
|
||||
FuelCost = fuelCost;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class AnomalyGeneratorGenerateButtonPressedEvent : BoundUserInterfaceMessage
|
||||
{
|
||||
|
||||
}
|
||||
319
Content.Shared/Anomaly/SharedAnomalySystem.cs
Normal file
@@ -0,0 +1,319 @@
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Anomaly.Components;
|
||||
using Content.Shared.Database;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Shared.Anomaly;
|
||||
|
||||
public abstract class SharedAnomalySystem : EntitySystem
|
||||
{
|
||||
[Dependency] protected readonly IGameTiming Timing = default!;
|
||||
[Dependency] private readonly INetManager _net = default!;
|
||||
[Dependency] protected readonly IRobustRandom Random = default!;
|
||||
[Dependency] protected readonly ISharedAdminLogManager Log = default!;
|
||||
[Dependency] protected readonly SharedAudioSystem Audio = default!;
|
||||
[Dependency] protected readonly SharedAppearanceSystem Appearance = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<AnomalyComponent, ComponentGetState>(OnAnomalyGetState);
|
||||
SubscribeLocalEvent<AnomalyComponent, ComponentHandleState>(OnAnomalyHandleState);
|
||||
SubscribeLocalEvent<AnomalySupercriticalComponent, ComponentGetState>(OnSupercriticalGetState);
|
||||
SubscribeLocalEvent<AnomalySupercriticalComponent, ComponentHandleState>(OnSupercriticalHandleState);
|
||||
|
||||
SubscribeLocalEvent<AnomalyComponent, EntityUnpausedEvent>(OnAnomalyUnpause);
|
||||
SubscribeLocalEvent<AnomalyPulsingComponent, EntityUnpausedEvent>(OnPulsingUnpause);
|
||||
SubscribeLocalEvent<AnomalySupercriticalComponent, EntityUnpausedEvent>(OnSupercriticalUnpause);
|
||||
}
|
||||
|
||||
private void OnAnomalyGetState(EntityUid uid, AnomalyComponent component, ref ComponentGetState args)
|
||||
{
|
||||
args.State = new AnomalyComponentState(
|
||||
component.Severity,
|
||||
component.Stability,
|
||||
component.Health,
|
||||
component.NextPulseTime);
|
||||
}
|
||||
|
||||
private void OnAnomalyHandleState(EntityUid uid, AnomalyComponent component, ref ComponentHandleState args)
|
||||
{
|
||||
if (args.Current is not AnomalyComponentState state)
|
||||
return;
|
||||
component.Severity = state.Severity;
|
||||
component.Stability = state.Stability;
|
||||
component.Health = state.Health;
|
||||
component.NextPulseTime = state.NextPulseTime;
|
||||
}
|
||||
|
||||
private void OnSupercriticalGetState(EntityUid uid, AnomalySupercriticalComponent component, ref ComponentGetState args)
|
||||
{
|
||||
args.State = new AnomalySupercriticalComponentState
|
||||
{
|
||||
EndTime = component.EndTime,
|
||||
Duration = component.SupercriticalDuration
|
||||
};
|
||||
}
|
||||
|
||||
private void OnSupercriticalHandleState(EntityUid uid, AnomalySupercriticalComponent component, ref ComponentHandleState args)
|
||||
{
|
||||
if (args.Current is not AnomalySupercriticalComponentState state)
|
||||
return;
|
||||
|
||||
component.EndTime = state.EndTime;
|
||||
component.SupercriticalDuration = state.Duration;
|
||||
}
|
||||
|
||||
private void OnAnomalyUnpause(EntityUid uid, AnomalyComponent component, ref EntityUnpausedEvent args)
|
||||
{
|
||||
component.NextPulseTime += args.PausedTime;
|
||||
Dirty(component);
|
||||
}
|
||||
|
||||
private void OnPulsingUnpause(EntityUid uid, AnomalyPulsingComponent component, ref EntityUnpausedEvent args)
|
||||
{
|
||||
component.EndTime += args.PausedTime;
|
||||
}
|
||||
|
||||
private void OnSupercriticalUnpause(EntityUid uid, AnomalySupercriticalComponent component, ref EntityUnpausedEvent args)
|
||||
{
|
||||
component.EndTime += args.PausedTime;
|
||||
Dirty(component);
|
||||
}
|
||||
|
||||
public void DoAnomalyPulse(EntityUid uid, AnomalyComponent? component = null)
|
||||
{
|
||||
if (!Resolve(uid, ref component))
|
||||
return;
|
||||
|
||||
var variation = Random.NextFloat(-component.PulseVariation, component.PulseVariation) + 1;
|
||||
component.NextPulseTime = Timing.CurTime + GetPulseLength(component) * variation;
|
||||
|
||||
// if we are above the growth threshold, then grow before the pulse
|
||||
if (component.Stability > component.GrowthThreshold)
|
||||
{
|
||||
ChangeAnomalySeverity(uid, GetSeverityIncreaseFromGrowth(component), component);
|
||||
}
|
||||
else
|
||||
{
|
||||
// just doing this to update the scanner ui
|
||||
// as they hook into these events
|
||||
ChangeAnomalySeverity(uid, 0);
|
||||
}
|
||||
|
||||
Log.Add(LogType.Anomaly, LogImpact.Medium, $"Anomaly {ToPrettyString(uid)} pulsed with severity {component.Severity}.");
|
||||
Audio.PlayPvs(component.PulseSound, uid);
|
||||
|
||||
var pulse = EnsureComp<AnomalyPulsingComponent>(uid);
|
||||
pulse.EndTime = Timing.CurTime + pulse.PulseDuration;
|
||||
Appearance.SetData(uid, AnomalyVisuals.IsPulsing, true);
|
||||
|
||||
var ev = new AnomalyPulseEvent(component.Stability, component.Severity);
|
||||
RaiseLocalEvent(uid, ref ev);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Begins the animation for going supercritical
|
||||
/// </summary>
|
||||
/// <param name="uid"></param>
|
||||
public void StartSupercriticalEvent(EntityUid uid)
|
||||
{
|
||||
// don't restart it if it's already begun
|
||||
if (HasComp<AnomalySupercriticalComponent>(uid))
|
||||
return;
|
||||
|
||||
Log.Add(LogType.Anomaly, LogImpact.High, $"Anomaly {ToPrettyString(uid)} began to go supercritical.");
|
||||
|
||||
var super = EnsureComp<AnomalySupercriticalComponent>(uid);
|
||||
super.EndTime = Timing.CurTime + super.SupercriticalDuration;
|
||||
Appearance.SetData(uid, AnomalyVisuals.Supercritical, true);
|
||||
Dirty(super);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Does the supercritical event for the anomaly.
|
||||
/// This isn't called once the anomaly reaches the point, but
|
||||
/// after the animation for it going supercritical
|
||||
/// </summary>
|
||||
/// <param name="uid"></param>
|
||||
/// <param name="component"></param>
|
||||
public void DoAnomalySupercriticalEvent(EntityUid uid, AnomalyComponent? component = null)
|
||||
{
|
||||
if (!Resolve(uid, ref component))
|
||||
return;
|
||||
Audio.PlayPvs(component.SupercriticalSound, uid);
|
||||
|
||||
var ev = new AnomalySupercriticalEvent();
|
||||
RaiseLocalEvent(uid, ref ev);
|
||||
|
||||
EndAnomaly(uid, component, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ends an anomaly, cleaning up all entities that may be associated with it.
|
||||
/// </summary>
|
||||
/// <param name="uid">The anomaly being shut down</param>
|
||||
/// <param name="component"></param>
|
||||
/// <param name="supercritical">Whether or not the anomaly ended via supercritical event</param>
|
||||
public void EndAnomaly(EntityUid uid, AnomalyComponent? component = null, bool supercritical = false)
|
||||
{
|
||||
if (!Resolve(uid, ref component))
|
||||
return;
|
||||
|
||||
var ev = new AnomalyShutdownEvent(uid, supercritical);
|
||||
RaiseLocalEvent(uid, ref ev, true);
|
||||
|
||||
Log.Add(LogType.Anomaly, LogImpact.Extreme, $"Anomaly {ToPrettyString(uid)} went supercritical.");
|
||||
|
||||
if (Terminating(uid) || _net.IsClient)
|
||||
return;
|
||||
Del(uid);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Changes the stability of the anomaly.
|
||||
/// </summary>
|
||||
/// <param name="uid"></param>
|
||||
/// <param name="change"></param>
|
||||
/// <param name="component"></param>
|
||||
public void ChangeAnomalyStability(EntityUid uid, float change, AnomalyComponent? component = null)
|
||||
{
|
||||
if (!Resolve(uid, ref component))
|
||||
return;
|
||||
|
||||
var newVal = component.Stability + change;
|
||||
|
||||
component.Stability = Math.Clamp(newVal, 0, 1);
|
||||
Dirty(component);
|
||||
|
||||
var ev = new AnomalyStabilityChangedEvent(uid, component.Stability);
|
||||
RaiseLocalEvent(uid, ref ev, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Changes the severity of an anomaly, going supercritical if it exceeds 1.
|
||||
/// </summary>
|
||||
/// <param name="uid"></param>
|
||||
/// <param name="change"></param>
|
||||
/// <param name="component"></param>
|
||||
public void ChangeAnomalySeverity(EntityUid uid, float change, AnomalyComponent? component = null)
|
||||
{
|
||||
if (!Resolve(uid, ref component))
|
||||
return;
|
||||
|
||||
var newVal = component.Severity + change;
|
||||
|
||||
if (newVal >= 1)
|
||||
StartSupercriticalEvent(uid);
|
||||
|
||||
component.Severity = Math.Clamp(newVal, 0, 1);
|
||||
Dirty(component);
|
||||
|
||||
var ev = new AnomalySeverityChangedEvent(uid, component.Severity);
|
||||
RaiseLocalEvent(uid, ref ev, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Changes the health of an anomaly, ending it if it's less than 0.
|
||||
/// </summary>
|
||||
/// <param name="uid"></param>
|
||||
/// <param name="change"></param>
|
||||
/// <param name="component"></param>
|
||||
public void ChangeAnomalyHealth(EntityUid uid, float change, AnomalyComponent? component = null)
|
||||
{
|
||||
if (!Resolve(uid, ref component))
|
||||
return;
|
||||
|
||||
var newVal = component.Health + change;
|
||||
|
||||
if (newVal < 0)
|
||||
{
|
||||
EndAnomaly(uid, component);
|
||||
return;
|
||||
}
|
||||
|
||||
component.Health = Math.Clamp(newVal, 0, 1);
|
||||
Dirty(component);
|
||||
|
||||
var ev = new AnomalyHealthChangedEvent(uid, component.Health);
|
||||
RaiseLocalEvent(uid, ref ev, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the length of time between each pulse
|
||||
/// for an anomaly based on its current stability.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For anomalies under the instability theshold, this will return the maximum length.
|
||||
/// For those over the theshold, they will return an amount between the maximum and
|
||||
/// minium value based on a linear relationship with the stability.
|
||||
/// </remarks>
|
||||
/// <param name="component"></param>
|
||||
/// <returns>The length of time as a TimeSpan, not including random variation.</returns>
|
||||
public TimeSpan GetPulseLength(AnomalyComponent component)
|
||||
{
|
||||
DebugTools.Assert(component.MaxPulseLength > component.MinPulseLength);
|
||||
var modifier = Math.Clamp((component.Stability - component.GrowthThreshold) / component.GrowthThreshold, 0, 1);
|
||||
return (component.MaxPulseLength - component.MinPulseLength) * modifier + component.MinPulseLength;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the increase in an anomaly's severity due
|
||||
/// to being above its growth threshold
|
||||
/// </summary>
|
||||
/// <param name="component"></param>
|
||||
/// <returns>The increase in severity for this anomaly</returns>
|
||||
private float GetSeverityIncreaseFromGrowth(AnomalyComponent component)
|
||||
{
|
||||
var score = 1 + Math.Max(component.Stability - component.GrowthThreshold, 0) * 10;
|
||||
return score * component.SeverityGrowthCoefficient;
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
foreach (var anomaly in EntityQuery<AnomalyComponent>())
|
||||
{
|
||||
var ent = anomaly.Owner;
|
||||
|
||||
// if the stability is under the death threshold,
|
||||
// update it every second to start killing it slowly.
|
||||
if (anomaly.Stability < anomaly.DecayThreshold)
|
||||
{
|
||||
ChangeAnomalyHealth(ent, anomaly.HealthChangePerSecond * frameTime, anomaly);
|
||||
}
|
||||
|
||||
if (Timing.CurTime > anomaly.NextPulseTime)
|
||||
{
|
||||
DoAnomalyPulse(ent, anomaly);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var pulse in EntityQuery<AnomalyPulsingComponent>())
|
||||
{
|
||||
var ent = pulse.Owner;
|
||||
|
||||
if (Timing.CurTime > pulse.EndTime)
|
||||
{
|
||||
Appearance.SetData(ent, AnomalyVisuals.IsPulsing, false);
|
||||
RemComp(ent, pulse);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var (super, anom) in EntityQuery<AnomalySupercriticalComponent, AnomalyComponent>())
|
||||
{
|
||||
var ent = anom.Owner;
|
||||
|
||||
if (Timing.CurTime <= super.EndTime)
|
||||
continue;
|
||||
DoAnomalySupercriticalEvent(ent, anom);
|
||||
RemComp(ent, super);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
using System.Threading;
|
||||
using Content.Shared.Construction.Prototypes;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
|
||||
|
||||
namespace Content.Shared.Singularity.Components;
|
||||
|
||||
@@ -25,9 +27,12 @@ public sealed class EmitterComponent : Component
|
||||
/// <summary>
|
||||
/// The entity that is spawned when the emitter fires.
|
||||
/// </summary>
|
||||
[DataField("boltType")]
|
||||
[DataField("boltType", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
public string BoltType = "EmitterBolt";
|
||||
|
||||
[DataField("selectableTypes", customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
|
||||
public List<string> SelectableTypes = new();
|
||||
|
||||
/// <summary>
|
||||
/// The current amount of power being used.
|
||||
/// </summary>
|
||||
|
||||
@@ -81,5 +81,7 @@ namespace Content.Shared.Verbs
|
||||
public static readonly VerbCategory SetSensor = new("verb-categories-set-sensor", null);
|
||||
|
||||
public static readonly VerbCategory Lever = new("verb-categories-lever", null);
|
||||
|
||||
public static readonly VerbCategory SelectType = new("verb-categories-select-type", null);
|
||||
}
|
||||
}
|
||||
|
||||
BIN
Resources/Audio/Ambience/Objects/anomaly_generator.ogg
Normal file
4
Resources/Audio/Ambience/Objects/attributions.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
- files: ["anomaly_generator.ogg"]
|
||||
license: "CC0-1.0"
|
||||
copyright: "Created by steaq, converted Mono and .ogg by EmoGarbage"
|
||||
source: "https://freesound.org/people/steaq/sounds/509249/"
|
||||
BIN
Resources/Audio/Ambience/anomaly_drone.ogg
Normal file
4
Resources/Audio/Ambience/attributions.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
- files: ["anomaly_drone.ogg"]
|
||||
license: "CC0-1.0"
|
||||
copyright: "Created by Joao_Janz, edited and converted to Mono by EmoGarbage"
|
||||
source: "https://freesound.org/people/Joao_Janz/sounds/478472/"
|
||||
@@ -1,3 +1,8 @@
|
||||
- files: ["beep.ogg"]
|
||||
license: "CC0-1.0"
|
||||
copyright: "Created by Pól, converted to OGG and Mono by EmoGarbage"
|
||||
source: "https://freesound.org/people/P%C3%B3l/sounds/385927/"
|
||||
|
||||
- files: ["trayhit1.ogg"]
|
||||
license: "CC-BY-SA-3.0"
|
||||
copyright: "Time immemorial"
|
||||
|
||||
BIN
Resources/Audio/Items/beep.ogg
Normal file
30
Resources/Locale/en-US/anomaly/anomaly.ftl
Normal file
@@ -0,0 +1,30 @@
|
||||
anomaly-vessel-component-anomaly-assigned = Anomaly assigned to vessel.
|
||||
anomaly-vessel-component-not-assigned = This vessel is not assigned to any anomaly. Try using a scanner on it.
|
||||
anomaly-vessel-component-assigned = This vessel is currently assigned to an anomaly.
|
||||
|
||||
anomaly-particles-delta = Delta particles
|
||||
anomaly-particles-epsilon = Epsilon particles
|
||||
anomaly-particles-zeta = Zeta particles
|
||||
|
||||
anomaly-scanner-component-scan-complete = Scan complete!
|
||||
|
||||
anomaly-scanner-ui-title = anomaly scanner
|
||||
anomaly-scanner-no-anomaly = No anomaly currently scanned.
|
||||
anomaly-scanner-severity-percentage = Current severity: [color=gray]{$percent}[/color]
|
||||
anomaly-scanner-stability-low = Current anomaly state: [color=gold]Decaying[/color]
|
||||
anomaly-scanner-stability-medium = Current anomaly state: [color=forestgreen]Stable[/color]
|
||||
anomaly-scanner-stability-high = Current anomaly state: [color=crimson]Growing[/color]
|
||||
anomaly-scanner-point-output = Approximate point output: [color=gray]{$point}[/color]
|
||||
anomaly-scanner-particle-readout = Particle Reaction Analysis:
|
||||
anomaly-scanner-particle-danger = - [color=crimson]Danger type:[/color] {$type}
|
||||
anomaly-scanner-particle-unstable = - [color=plum]Unstable type:[/color] {$type}
|
||||
anomaly-scanner-particle-containment = - [color=goldenrod]Containment type:[/color] {$type}
|
||||
anomaly-scanner-pulse-timer = Time until next pulse: [color=gray]{$time}[/color]
|
||||
|
||||
anomaly-generator-ui-title = anomaly generator
|
||||
anomaly-generator-fuel-display = Fuel:
|
||||
anomaly-generator-cooldown = Cooldown: [color=gray]{$time}[/color]
|
||||
anomaly-generator-no-cooldown = Cooldown: [color=gray]Complete[/color]
|
||||
anomaly-generator-yes-fire = Status: [color=forestgreen]Ready[/color]
|
||||
anomaly-generator-no-fire = Status: [color=crimson]Not ready[/color]
|
||||
anomaly-generator-generate = Generate Anomaly
|
||||
@@ -73,6 +73,9 @@ technologies-super-powercell-printing-description = Print super powercells.
|
||||
technologies-scientific-technology = Scientific technology
|
||||
technologies-scientific-technology-description = The basics of a research team's supplies.
|
||||
|
||||
technologies-anomaly-technology = Anomaly technology
|
||||
technologies-anomaly-technology-description = Machines for advanced anomaly containment.
|
||||
|
||||
technologies-robotics-technology = Robotics technology
|
||||
technologies-robotics-technology-description = Parts needed for constructing mechanized friends.
|
||||
|
||||
|
||||
@@ -13,3 +13,6 @@ comp-emitter-not-anchored = The {$target} isn't anchored to the ground!
|
||||
|
||||
# Upgrades
|
||||
emitter-component-upgrade-fire-rate = fire rate
|
||||
|
||||
emitter-component-current-type = The current selected type is: {$type}.
|
||||
emitter-component-type-set = Type set to: {$type}
|
||||
@@ -26,6 +26,7 @@ verb-categories-channel-select = Channels
|
||||
verb-categories-set-sensor = Sensor
|
||||
verb-categories-timer = Set Delay
|
||||
verb-categories-lever = Lever
|
||||
verb-categories-select-type = Select Type
|
||||
verb-categories-fax = Set Destination
|
||||
|
||||
verb-common-toggle-light = Toggle light
|
||||
|
||||
@@ -9,3 +9,9 @@
|
||||
- id: ClothingHeadsetScience
|
||||
- id: ClothingMaskSterile
|
||||
- id: ClothingOuterCoatLab
|
||||
- id: AnomalyScanner
|
||||
prob: 0.5
|
||||
orGroup: Scanner
|
||||
- id: NodeScanner
|
||||
prob: 0.5
|
||||
orGroup: Scanner
|
||||
|
||||
@@ -518,6 +518,21 @@
|
||||
- MicroManipulatorStockPart
|
||||
- ScanningModuleStockPart
|
||||
- NodeScanner
|
||||
- AnomalyScanner
|
||||
|
||||
- type: technology
|
||||
name: technologies-anomaly-technology
|
||||
id: AnomalyTechnology
|
||||
description: technologies-anomaly-technology-description
|
||||
icon:
|
||||
sprite: Structures/Machines/Anomaly/ape.rsi
|
||||
state: base
|
||||
requiredPoints: 10000
|
||||
requiredTechnologies:
|
||||
- ScientificTechnology
|
||||
unlockedRecipes:
|
||||
- AnomalyVesselCircuitboard
|
||||
- APECircuitboard
|
||||
|
||||
- type: technology
|
||||
name: technologies-robotics-technology
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
- type: entity
|
||||
id: RandomAnomalySpawner
|
||||
name: random anomaly spawner
|
||||
parent: MarkerBase
|
||||
components:
|
||||
- type: Sprite
|
||||
layers:
|
||||
- state: red
|
||||
- sprite: Structures/Specific/anomaly.rsi
|
||||
state: anom1
|
||||
- type: RandomSpawner
|
||||
prototypes:
|
||||
- AnomalyPyroclastic
|
||||
- AnomalyGravity
|
||||
- AnomalyElectricity
|
||||
chance: 1
|
||||
@@ -179,6 +179,38 @@
|
||||
Steel: 5
|
||||
Cable: 1
|
||||
|
||||
- type: entity
|
||||
parent: BaseMachineCircuitboard
|
||||
id: AnomalyVesselCircuitboard
|
||||
name: anomaly vessel machine board
|
||||
description: A machine printed circuit board for an anomaly vessel
|
||||
components:
|
||||
- type: Sprite
|
||||
state: science
|
||||
- type: MachineBoard
|
||||
prototype: MachineAnomalyVessel
|
||||
requirements:
|
||||
ScanningModule: 5
|
||||
materialRequirements:
|
||||
Cable: 1
|
||||
PlasmaGlass: 10
|
||||
|
||||
- type: entity
|
||||
parent: BaseMachineCircuitboard
|
||||
id: APECircuitboard
|
||||
name: A.P.E. machine board
|
||||
description: A machine printed circuit board for an A.P.E.
|
||||
components:
|
||||
- type: Sprite
|
||||
state: science
|
||||
- type: MachineBoard
|
||||
prototype: MachineAPE
|
||||
requirements:
|
||||
Capacitor: 1
|
||||
Laser: 3
|
||||
materialRequirements:
|
||||
Cable: 1
|
||||
|
||||
- type: entity
|
||||
id: ThermomachineFreezerMachineCircuitBoard
|
||||
parent: BaseMachineCircuitboard
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
- type: entity
|
||||
parent: BaseItem
|
||||
id: AnomalyScanner
|
||||
name: anomaly scanner
|
||||
description: A hand-held scanner built to collect information on various anomalous objects.
|
||||
components:
|
||||
- type: Sprite
|
||||
sprite: Objects/Specific/Research/anomalyscanner.rsi
|
||||
netsync: false
|
||||
state: icon
|
||||
- type: ActivatableUI
|
||||
key: enum.AnomalyScannerUiKey.Key
|
||||
closeOnHandDeselect: false
|
||||
inHandsOnly: true
|
||||
- type: UserInterface
|
||||
interfaces:
|
||||
- key: enum.AnomalyScannerUiKey.Key
|
||||
type: AnomalyScannerBoundUserInterface
|
||||
- type: AnomalyScanner
|
||||
@@ -269,6 +269,58 @@
|
||||
- type: TimedDespawn
|
||||
lifetime: 0.4
|
||||
|
||||
- type: entity
|
||||
parent: BaseBullet
|
||||
id: AnomalousParticleDelta
|
||||
name: delta particles
|
||||
noSpawn: true
|
||||
components:
|
||||
- type: AnomalousParticle
|
||||
particleType: Delta
|
||||
- type: Sprite
|
||||
sprite: Objects/Weapons/Guns/Projectiles/magic.rsi
|
||||
layers:
|
||||
- state: magicm
|
||||
shader: unshaded
|
||||
- type: Ammo
|
||||
muzzleFlash: null
|
||||
- type: Physics
|
||||
- type: Fixtures
|
||||
fixtures:
|
||||
- shape:
|
||||
!type:PhysShapeAabb
|
||||
bounds: "-0.2,-0.2,0.2,0.2"
|
||||
hard: false
|
||||
id: projectile
|
||||
mask:
|
||||
- Impassable
|
||||
- Opaque
|
||||
- *flybyfixture
|
||||
- type: Projectile
|
||||
damage:
|
||||
types:
|
||||
Heat: 3
|
||||
- type: TimedDespawn
|
||||
lifetime: 3
|
||||
|
||||
- type: entity
|
||||
parent: AnomalousParticleDelta
|
||||
id: AnomalousParticleEpsilon
|
||||
name: epsilon particles
|
||||
noSpawn: true
|
||||
components:
|
||||
- type: AnomalousParticle
|
||||
particleType: Epsilon
|
||||
|
||||
- type: entity
|
||||
parent: AnomalousParticleDelta
|
||||
id: AnomalousParticleZeta
|
||||
name: zeta particles
|
||||
noSpawn: true
|
||||
components:
|
||||
- type: AnomalousParticle
|
||||
particleType: Zeta
|
||||
|
||||
# Launcher projectiles (grenade / rocket)
|
||||
- type: entity
|
||||
id: BulletRocket
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
- type: entity
|
||||
id: MachineAnomalyVessel
|
||||
parent: [ BaseMachinePowered, ConstructibleMachine ]
|
||||
name: anomaly vessel
|
||||
description: A container able to harness a scan of an anomaly and turn it into research points.
|
||||
components:
|
||||
- type: Sprite
|
||||
noRot: true
|
||||
sprite: Structures/Machines/Anomaly/anomaly_vessel.rsi
|
||||
layers:
|
||||
- state: base
|
||||
- state: powered
|
||||
shader: unshaded
|
||||
map: ["enum.PowerDeviceVisualLayers.Powered"]
|
||||
- state: anomaly
|
||||
shader: unshaded
|
||||
map: ["enum.AnomalyVesselVisualLayers.Base"]
|
||||
- state: panel
|
||||
map: ["enum.WiresVisualLayers.MaintenancePanel"]
|
||||
- type: Transform
|
||||
noRot: false
|
||||
- type: AnomalyVessel
|
||||
- type: ResearchClient
|
||||
- type: ActivatableUI
|
||||
key: enum.ResearchClientUiKey.Key
|
||||
- type: ActivatableUIRequiresPower
|
||||
- type: UserInterface
|
||||
interfaces:
|
||||
- key: enum.ResearchClientUiKey.Key
|
||||
type: ResearchClientBoundUserInterface
|
||||
- type: Machine
|
||||
board: AnomalyVesselCircuitboard
|
||||
- type: PointLight
|
||||
radius: 1.2
|
||||
energy: 2
|
||||
color: "#fca3c0"
|
||||
- type: Appearance
|
||||
- type: Wires
|
||||
BoardName: "Vessel"
|
||||
LayoutId: Vessel
|
||||
- type: AmbientSound
|
||||
enabled: false
|
||||
range: 3
|
||||
volume: -8
|
||||
sound:
|
||||
path: /Audio/Ambience/anomaly_drone.ogg
|
||||
- type: GenericVisualizer
|
||||
visuals:
|
||||
enum.PowerDeviceVisuals.Powered:
|
||||
enum.PowerDeviceVisualLayers.Powered:
|
||||
True: { visible: true }
|
||||
False: { visible: false }
|
||||
enum.AnomalyVesselVisuals.HasAnomaly:
|
||||
enum.AnomalyVesselVisualLayers.Base:
|
||||
True: { visible: true }
|
||||
False: { visible: false }
|
||||
enum.WiresVisuals.MaintenancePanelState:
|
||||
enum.WiresVisualLayers.MaintenancePanel:
|
||||
True: { visible: false }
|
||||
False: { visible: true }
|
||||
- type: Explosive
|
||||
explosionType: Default
|
||||
maxIntensity: 20
|
||||
intensitySlope: 30
|
||||
totalIntensity: 30
|
||||
canCreateVacuum: false
|
||||
- type: Destructible
|
||||
thresholds:
|
||||
- trigger:
|
||||
!type:DamageTrigger
|
||||
damage: 150
|
||||
behaviors:
|
||||
- !type:DoActsBehavior
|
||||
acts: ["Destruction"]
|
||||
- !type:PlaySoundBehavior
|
||||
sound:
|
||||
path: /Audio/Effects/metalbreak.ogg
|
||||
- !type:ExplodeBehavior
|
||||
|
||||
- type: entity
|
||||
id: MachineAPE
|
||||
parent: [ BaseMachinePowered, ConstructibleMachine ]
|
||||
name: A.P.E.
|
||||
description: An Anomalous Particle Emitter, capable of shooting out unstable particles which can interface with anomalies.
|
||||
components:
|
||||
- type: Sprite
|
||||
noRot: true
|
||||
sprite: Structures/Machines/Anomaly/ape.rsi
|
||||
layers:
|
||||
- state: base
|
||||
- state: unshaded
|
||||
shader: unshaded
|
||||
map: ["enum.PowerDeviceVisualLayers.Powered"]
|
||||
- state: panel
|
||||
map: ["enum.WiresVisualLayers.MaintenancePanel"]
|
||||
- state: firing
|
||||
shader: unshaded
|
||||
visible: false
|
||||
map: ["enum.EmitterVisualLayers.Lights"]
|
||||
- state: locked
|
||||
shader: unshaded
|
||||
visible: false
|
||||
map: ["enum.StorageVisualLayers.Lock"]
|
||||
- type: Transform
|
||||
noRot: false
|
||||
- type: Fixtures
|
||||
fixtures:
|
||||
- shape:
|
||||
!type:PhysShapeCircle
|
||||
radius: 0.35
|
||||
density: 190
|
||||
mask:
|
||||
- MachineMask
|
||||
layer:
|
||||
- MachineLayer
|
||||
- type: Rotatable
|
||||
rotateWhileAnchored: true
|
||||
- type: Machine
|
||||
board: APECircuitboard
|
||||
- type: Lock
|
||||
locked: false
|
||||
- type: AccessReader
|
||||
access: [[ "Research" ]]
|
||||
- type: Emitter
|
||||
onState: firing
|
||||
powerUseActive: 100
|
||||
boltType: AnomalousParticleDelta
|
||||
underpoweredState: underpowered
|
||||
selectableTypes:
|
||||
- AnomalousParticleDelta
|
||||
- AnomalousParticleEpsilon
|
||||
- AnomalousParticleZeta
|
||||
fireBurstSize: 1
|
||||
baseFireBurstDelayMin: 2
|
||||
baseFireBurstDelayMax: 6
|
||||
- type: ApcPowerReceiver
|
||||
powerLoad: 100
|
||||
- type: Gun
|
||||
fireRate: 10 #just has to be fast enough to keep up with upgrades
|
||||
showExamineText: false
|
||||
selectedMode: SemiAuto
|
||||
availableModes:
|
||||
- SemiAuto
|
||||
soundGunshot:
|
||||
path: /Audio/Weapons/Guns/Gunshots/taser2.ogg
|
||||
- type: Appearance
|
||||
- type: WiresVisuals
|
||||
- type: Wires
|
||||
BoardName: "Ape"
|
||||
LayoutId: Ape
|
||||
- type: GenericVisualizer
|
||||
visuals:
|
||||
enum.PowerDeviceVisuals.Powered:
|
||||
enum.PowerDeviceVisualLayers.Powered:
|
||||
True: { visible: true }
|
||||
False: { visible: false }
|
||||
|
||||
- type: entity
|
||||
id: MachineAnomalyGenerator
|
||||
parent: BaseMachinePowered
|
||||
name: anomaly generator
|
||||
description: The peak of psuedoscientific technology.
|
||||
placement:
|
||||
mode: AlignTileAny
|
||||
components:
|
||||
- type: Sprite
|
||||
netsync: false
|
||||
sprite: Structures/Machines/Anomaly/anomaly_generator.rsi
|
||||
snapCardinals: true
|
||||
layers:
|
||||
- state: base
|
||||
- state: panel
|
||||
map: ["enum.WiresVisualLayers.MaintenancePanel"]
|
||||
visible: false
|
||||
- state: unshaded
|
||||
shader: unshaded
|
||||
map: ["enum.PowerDeviceVisualLayers.Powered"]
|
||||
- state: inserting
|
||||
visible: false
|
||||
map: ["enum.MaterialStorageVisualLayers.Inserting"]
|
||||
- type: Transform
|
||||
anchored: true
|
||||
- type: ApcPowerReceiver
|
||||
powerLoad: 1500
|
||||
- type: ExtensionCableReceiver
|
||||
- type: AmbientSound
|
||||
range: 5
|
||||
volume: -3
|
||||
sound:
|
||||
path: /Audio/Ambience/Objects/anomaly_generator.ogg
|
||||
- type: Physics
|
||||
bodyType: Static
|
||||
- type: AnomalyGenerator
|
||||
- type: MaterialStorage
|
||||
whitelist:
|
||||
tags:
|
||||
- Sheet
|
||||
materialWhiteList:
|
||||
- Plasma
|
||||
- type: Fixtures
|
||||
fixtures:
|
||||
- shape:
|
||||
!type:PhysShapeAabb
|
||||
bounds: "-1.3,-1.3,1.3,1.3"
|
||||
density: 50
|
||||
mask:
|
||||
- LargeMobMask
|
||||
layer:
|
||||
- WallLayer
|
||||
- type: Repairable
|
||||
fuelCost: 10
|
||||
doAfterDelay: 5
|
||||
- type: Wires
|
||||
BoardName: "AnomalyGenerator"
|
||||
LayoutId: AnomalyGenerator
|
||||
- type: Destructible
|
||||
thresholds:
|
||||
- trigger:
|
||||
!type:DamageTrigger
|
||||
damage: 500
|
||||
behaviors:
|
||||
- !type:DoActsBehavior
|
||||
acts: ["Breakage"]
|
||||
- type: ActivatableUI
|
||||
key: enum.AnomalyGeneratorUiKey.Key
|
||||
- type: ActivatableUIRequiresPower
|
||||
- type: UserInterface
|
||||
interfaces:
|
||||
- key: enum.AnomalyGeneratorUiKey.Key
|
||||
type: AnomalyGeneratorBoundUserInterface
|
||||
- type: Appearance
|
||||
- type: GenericVisualizer
|
||||
visuals:
|
||||
enum.PowerDeviceVisuals.Powered:
|
||||
enum.PowerDeviceVisualLayers.Powered:
|
||||
True: { visible: true }
|
||||
False: { visible: false }
|
||||
- type: WiresVisuals
|
||||
- type: StaticPrice
|
||||
price: 5000
|
||||
@@ -169,6 +169,7 @@
|
||||
- MiningDrill
|
||||
- ConveyorBeltAssembly
|
||||
- AppraisalTool
|
||||
- AnomalyScanner
|
||||
- RCD
|
||||
- RCDAmmo
|
||||
- HydroponicsToolScythe
|
||||
@@ -314,6 +315,8 @@
|
||||
- SeedExtractorMachineCircuitboard
|
||||
- AnalysisComputerCircuitboard
|
||||
- ExosuitFabricatorMachineCircuitboard
|
||||
- AnomalyVesselCircuitboard
|
||||
- APECircuitboard
|
||||
- ArtifactAnalyzerMachineCircuitboard
|
||||
- TraversalDistorterMachineCircuitboard
|
||||
- BoozeDispenserMachineCircuitboard
|
||||
|
||||
104
Resources/Prototypes/Entities/Structures/Specific/anomalies.yml
Normal file
@@ -0,0 +1,104 @@
|
||||
- type: entity
|
||||
abstract: true
|
||||
id: BaseAnomaly
|
||||
name: anomaly
|
||||
description: A impossible object in space. Should you be standing this close to it?
|
||||
components:
|
||||
- type: Anomaly
|
||||
pulseSound:
|
||||
collection: RadiationPulse
|
||||
params:
|
||||
volume: 5
|
||||
- type: AmbientSound
|
||||
range: 5
|
||||
volume: -5
|
||||
sound:
|
||||
path: /Audio/Ambience/anomaly_drone.ogg
|
||||
- type: Transform
|
||||
anchored: true
|
||||
- type: Physics
|
||||
bodyType: Static
|
||||
- type: Fixtures
|
||||
fixtures:
|
||||
- shape:
|
||||
!type:PhysShapeCircle
|
||||
radius: 0.35
|
||||
density: 50
|
||||
mask:
|
||||
- MobMask
|
||||
layer:
|
||||
- MobLayer
|
||||
- type: Sprite
|
||||
netsync: false
|
||||
drawdepth: Items
|
||||
sprite: Structures/Specific/anomaly.rsi
|
||||
- type: InteractionOutline
|
||||
- type: Clickable
|
||||
- type: Damageable
|
||||
damageContainer: Inorganic
|
||||
damageModifierSet: Metallic
|
||||
- type: Appearance
|
||||
- type: EmitSoundOnSpawn
|
||||
sound:
|
||||
path: /Audio/Effects/teleport_arrival.ogg
|
||||
|
||||
- type: entity
|
||||
id: AnomalyPyroclastic
|
||||
parent: BaseAnomaly
|
||||
suffix: Pyroclastic
|
||||
components:
|
||||
- type: Sprite
|
||||
layers:
|
||||
- state: anom1
|
||||
map: ["enum.AnomalyVisualLayers.Base"]
|
||||
- state: anom1-pulse
|
||||
map: ["enum.AnomalyVisualLayers.Animated"]
|
||||
visible: false
|
||||
- type: PointLight
|
||||
radius: 2.0
|
||||
energy: 7.5
|
||||
color: "#fca3c0"
|
||||
castShadows: false
|
||||
- type: PyroclasticAnomaly
|
||||
|
||||
- type: entity
|
||||
id: AnomalyGravity
|
||||
parent: BaseAnomaly
|
||||
suffix: Gravity
|
||||
components:
|
||||
- type: Sprite
|
||||
drawdepth: Effects #it needs to draw over stuff.
|
||||
layers:
|
||||
- state: anom2
|
||||
map: ["enum.AnomalyVisualLayers.Base"]
|
||||
- state: anom2-pulse
|
||||
map: ["enum.AnomalyVisualLayers.Animated"]
|
||||
visible: false
|
||||
- type: PointLight
|
||||
radius: 5.0
|
||||
energy: 20
|
||||
color: "#1e070e"
|
||||
castShadows: false
|
||||
- type: GravityAnomaly
|
||||
- type: GravityWell
|
||||
- type: RadiationSource
|
||||
|
||||
- type: entity
|
||||
id: AnomalyElectricity
|
||||
parent: BaseAnomaly
|
||||
suffix: Electricity
|
||||
components:
|
||||
- type: Sprite
|
||||
layers:
|
||||
- state: anom3
|
||||
map: ["enum.AnomalyVisualLayers.Base"]
|
||||
- state: anom3-pulse
|
||||
map: ["enum.AnomalyVisualLayers.Animated"]
|
||||
visible: false
|
||||
- type: PointLight
|
||||
radius: 2.0
|
||||
energy: 5.0
|
||||
color: "#ffffaa"
|
||||
castShadows: false
|
||||
- type: ElectricityAnomaly
|
||||
- type: Electrified
|
||||
@@ -246,12 +246,21 @@
|
||||
- type: entity
|
||||
parent: BaseSign
|
||||
id: SignAnomaly
|
||||
name: xeno-archeology lab sign
|
||||
description: A sign indicating the xeno-archeology lab.
|
||||
name: xenoarcheology lab sign
|
||||
description: A sign indicating the xenoarcheology lab.
|
||||
components:
|
||||
- type: Sprite
|
||||
state: anomaly
|
||||
|
||||
- type: entity
|
||||
parent: BaseSign
|
||||
id: SignAnomaly2
|
||||
name: anomaly lab sign
|
||||
description: A sign indicating the anomalous research lab.
|
||||
components:
|
||||
- type: Sprite
|
||||
state: anomaly2
|
||||
|
||||
- type: entity
|
||||
parent: BaseSign
|
||||
id: SignAtmos
|
||||
|
||||
@@ -48,3 +48,11 @@
|
||||
Steel: 100
|
||||
Plastic: 200
|
||||
Glass: 100
|
||||
|
||||
- type: latheRecipe
|
||||
id: AnomalyScanner
|
||||
result: AnomalyScanner
|
||||
completetime: 2
|
||||
materials:
|
||||
Plastic: 200
|
||||
Glass: 150
|
||||
@@ -202,6 +202,22 @@
|
||||
Glass: 900
|
||||
Gold: 100
|
||||
|
||||
- type: latheRecipe
|
||||
id: AnomalyVesselCircuitboard
|
||||
result: AnomalyVesselCircuitboard
|
||||
completetime: 4
|
||||
materials:
|
||||
Steel: 100
|
||||
Glass: 900
|
||||
|
||||
- type: latheRecipe
|
||||
id: APECircuitboard
|
||||
result: APECircuitboard
|
||||
completetime: 4
|
||||
materials:
|
||||
Steel: 100
|
||||
Glass: 900
|
||||
|
||||
- type: latheRecipe
|
||||
id: ReagentGrinderMachineCircuitboard
|
||||
result: ReagentGrinderMachineCircuitboard
|
||||
|
||||
|
After Width: | Height: | Size: 942 B |
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"version": 1,
|
||||
"size": {
|
||||
"x": 32,
|
||||
"y": 32
|
||||
},
|
||||
"license": "CC0-1.0",
|
||||
"copyright": "Created by EmoGarbage",
|
||||
"states": [
|
||||
{
|
||||
"name": "icon"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 609 B |
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"version":1,
|
||||
"size":{
|
||||
"x":96,
|
||||
"y":96
|
||||
},
|
||||
"license":"CC0-1.0",
|
||||
"copyright":"Created by EmoGarbage",
|
||||
"states":[
|
||||
{
|
||||
"name":"base"
|
||||
},
|
||||
{
|
||||
"name": "inserting",
|
||||
"delays": [
|
||||
[
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "panel"
|
||||
},
|
||||
{
|
||||
"name": "unshaded",
|
||||
"delays": [
|
||||
[
|
||||
1.0,
|
||||
0.25,
|
||||
0.25,
|
||||
0.25,
|
||||
0.25,
|
||||
0.25,
|
||||
0.25,
|
||||
0.5
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 347 B |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 283 B |
|
After Width: | Height: | Size: 615 B |
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"version": 1,
|
||||
"license": "CC0-1.0",
|
||||
"copyright": "Created by EmoGarbage404",
|
||||
"size": {
|
||||
"x": 32,
|
||||
"y": 32
|
||||
},
|
||||
"states": [
|
||||
{
|
||||
"name": "anomaly"
|
||||
},
|
||||
{
|
||||
"name": "base"
|
||||
},
|
||||
{
|
||||
"name": "panel"
|
||||
},
|
||||
{
|
||||
"name": "powered"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 228 B |
|
After Width: | Height: | Size: 133 B |
BIN
Resources/Textures/Structures/Machines/Anomaly/ape.rsi/base.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 306 B |
|
After Width: | Height: | Size: 142 B |
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"version": 1,
|
||||
"license": "CC0-1.0",
|
||||
"copyright": "Created by EmoGarbage404",
|
||||
"size": {
|
||||
"x": 32,
|
||||
"y": 32
|
||||
},
|
||||
"states": [
|
||||
{
|
||||
"name": "base",
|
||||
"directions": 4
|
||||
},
|
||||
{
|
||||
"name": "firing",
|
||||
"directions": 4
|
||||
},
|
||||
{
|
||||
"name": "locked",
|
||||
"directions": 4
|
||||
},
|
||||
{
|
||||
"name": "panel",
|
||||
"directions": 4
|
||||
},
|
||||
{
|
||||
"name": "underpowered",
|
||||
"directions": 4
|
||||
},
|
||||
{
|
||||
"name": "unshaded",
|
||||
"directions": 4
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
Resources/Textures/Structures/Machines/Anomaly/ape.rsi/panel.png
Normal file
|
After Width: | Height: | Size: 289 B |
|
After Width: | Height: | Size: 172 B |
|
After Width: | Height: | Size: 283 B |
|
After Width: | Height: | Size: 2.6 KiB |
BIN
Resources/Textures/Structures/Specific/anomaly.rsi/anom1.png
Normal file
|
After Width: | Height: | Size: 663 B |
|
After Width: | Height: | Size: 2.2 KiB |
BIN
Resources/Textures/Structures/Specific/anomaly.rsi/anom2.png
Normal file
|
After Width: | Height: | Size: 379 B |
|
After Width: | Height: | Size: 4.1 KiB |
BIN
Resources/Textures/Structures/Specific/anomaly.rsi/anom3.png
Normal file
|
After Width: | Height: | Size: 658 B |
|
After Width: | Height: | Size: 1.7 KiB |
BIN
Resources/Textures/Structures/Specific/anomaly.rsi/anom4.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
97
Resources/Textures/Structures/Specific/anomaly.rsi/meta.json
Normal file
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"version": 1,
|
||||
"license": "CC0-1.0",
|
||||
"copyright": "Created by EmoGarbage; anom3, anom3-pulse, anom4, anom4-pulse are CC-BY-SA-3.0 at https://github.com/ParadiseSS13/Paradise/blob/master/icons/effects/effects.dmi",
|
||||
"size": {
|
||||
"x": 32,
|
||||
"y": 32
|
||||
},
|
||||
"states": [
|
||||
{
|
||||
"name": "anom1"
|
||||
},
|
||||
{
|
||||
"name": "anom1-pulse",
|
||||
"delays": [
|
||||
[
|
||||
0.15625,
|
||||
0.15625,
|
||||
0.15625,
|
||||
0.15625,
|
||||
0.15625,
|
||||
0.15625,
|
||||
0.15625,
|
||||
0.15625
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "anom2"
|
||||
},
|
||||
{
|
||||
"name": "anom2-pulse",
|
||||
"delays": [
|
||||
[
|
||||
0.15625,
|
||||
0.15625,
|
||||
0.15625,
|
||||
0.15625,
|
||||
0.15625,
|
||||
0.15625,
|
||||
0.15625,
|
||||
0.15625
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "anom3"
|
||||
},
|
||||
{
|
||||
"name": "anom3-pulse",
|
||||
"delays": [
|
||||
[
|
||||
0.2,
|
||||
0.2,
|
||||
0.2,
|
||||
0.2,
|
||||
0.2,
|
||||
0.2,
|
||||
0.2,
|
||||
0.2,
|
||||
0.2,
|
||||
0.2,
|
||||
0.2,
|
||||
0.2
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "anom4",
|
||||
"delays": [
|
||||
[
|
||||
0.3,
|
||||
0.3,
|
||||
0.3,
|
||||
0.3,
|
||||
0.3,
|
||||
0.3,
|
||||
0.3
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "anom4-pulse",
|
||||
"delays": [
|
||||
[
|
||||
0.15,
|
||||
0.15,
|
||||
0.15,
|
||||
0.15,
|
||||
0.15,
|
||||
0.15,
|
||||
0.15
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
Resources/Textures/Structures/Wallmounts/signs.rsi/anomaly2.png
Normal file
|
After Width: | Height: | Size: 454 B |
@@ -23,6 +23,9 @@
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "anomaly2"
|
||||
},
|
||||
{
|
||||
"name": "armory",
|
||||
"delays": [
|
||||
|
||||