This commit is contained in:
Nemanja
2023-01-17 00:05:20 -05:00
committed by GitHub
parent 06f19dafc9
commit 9cd0c11870
85 changed files with 3109 additions and 54 deletions

View 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);
}
}
}
}

View 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
}

View File

@@ -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));
}
}

View 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>

View 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();
}
}

View File

@@ -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();
}
}

View 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>

View 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();
}
}

View 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);
}
}

View 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;
}
}

View 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);
}
}

View 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()
};
}
}

View File

@@ -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";
}

View File

@@ -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";
}

View 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
{
}

View 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;
}

View 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);
}
}
}

View 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;
}
}

View 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);
}
}
}

View File

@@ -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,52 +76,52 @@ 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,
shape,
"BeamBody",
hard: false,
collisionMask: (int)CollisionGroup.ItemMask,
collisionLayer: (int)CollisionGroup.MobLayer,
manager: manager,
body: physics);
_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);
if (controller != null)
beam.VirtualBeamController = controller;
else
{
FixturesComponent? manager = null;
_fixture.TryCreateFixture(
ent,
shape,
"BeamBody",
hard: false,
collisionMask: (int)CollisionGroup.ItemMask,
collisionLayer: (int)CollisionGroup.MobLayer,
manager: manager,
body: physics);
var controllerEnt = Spawn("VirtualBeamEntityController", beamSpawnPos);
beam.VirtualBeamController = controllerEnt;
_physics.SetBodyType(ent, BodyType.Dynamic, manager: manager, body: physics);
_physics.SetCanCollide(ent, true, manager: manager, body: physics);
_audio.PlayPvs(beam.Sound, beam.Owner);
var beamVisualizerEvent = new BeamVisualizerEvent(ent, distanceLength, userAngle, bodyState, shader);
RaiseNetworkEvent(beamVisualizerEvent);
if (controller != null)
beam.VirtualBeamController = controller;
else
{
var controllerEnt = Spawn("VirtualBeamEntityController", beamSpawnPos);
beam.VirtualBeamController = controllerEnt;
_audio.PlayPvs(beam.Sound, beam.Owner);
var beamControllerCreatedEvent = new BeamControllerCreatedEvent(ent, controllerEnt);
RaiseLocalEvent(controllerEnt, beamControllerCreatedEvent);
}
//Create the rest of the beam, sprites handled through the BeamVisualizerEvent
for (int i = 0; i < distanceLength-1; i++)
{
beamSpawnPos = beamSpawnPos.Offset(calculatedDistance.Normalized);
var newEnt = Spawn(prototype, beamSpawnPos);
var ev = new BeamVisualizerEvent(newEnt, distanceLength, userAngle, bodyState, shader);
RaiseNetworkEvent(ev);
}
var beamFiredEvent = new BeamFiredEvent(ent);
RaiseLocalEvent(beam.VirtualBeamController.Value, beamFiredEvent);
var beamControllerCreatedEvent = new BeamControllerCreatedEvent(ent, controllerEnt);
RaiseLocalEvent(controllerEnt, beamControllerCreatedEvent);
}
//Create the rest of the beam, sprites handled through the BeamVisualizerEvent
for (var i = 0; i < distanceLength-1; i++)
{
beamSpawnPos = beamSpawnPos.Offset(calculatedDistance.Normalized);
var newEnt = Spawn(prototype, beamSpawnPos);
var ev = new BeamVisualizerEvent(newEnt, distanceLength, userAngle, bodyState, shader);
RaiseNetworkEvent(ev);
}
var beamFiredEvent = new BeamFiredEvent(ent);
RaiseLocalEvent(beam.VirtualBeamController.Value, beamFiredEvent);
}
/// <summary>

View File

@@ -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);

View File

@@ -81,4 +81,5 @@ public enum LogType
Stamina = 76,
EntitySpawn = 77,
AdminMessage = 78,
Anomaly = 79
}

View 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);

View 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);
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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;
}

View 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);
}
}
}

View 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
{
}

View 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);
}
}
}

View File

@@ -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>

View File

@@ -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);
}
}

Binary file not shown.

View 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/"

Binary file not shown.

View 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/"

View File

@@ -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"

Binary file not shown.

View 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

View File

@@ -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.

View File

@@ -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}

View File

@@ -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

View File

@@ -9,3 +9,9 @@
- id: ClothingHeadsetScience
- id: ClothingMaskSterile
- id: ClothingOuterCoatLab
- id: AnomalyScanner
prob: 0.5
orGroup: Scanner
- id: NodeScanner
prob: 0.5
orGroup: Scanner

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -169,6 +169,7 @@
- MiningDrill
- ConveyorBeltAssembly
- AppraisalTool
- AnomalyScanner
- RCD
- RCDAmmo
- HydroponicsToolScythe
@@ -314,6 +315,8 @@
- SeedExtractorMachineCircuitboard
- AnalysisComputerCircuitboard
- ExosuitFabricatorMachineCircuitboard
- AnomalyVesselCircuitboard
- APECircuitboard
- ArtifactAnalyzerMachineCircuitboard
- TraversalDistorterMachineCircuitboard
- BoozeDispenserMachineCircuitboard

View 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

View File

@@ -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

View File

@@ -48,3 +48,11 @@
Steel: 100
Plastic: 200
Glass: 100
- type: latheRecipe
id: AnomalyScanner
result: AnomalyScanner
completetime: 2
materials:
Plastic: 200
Glass: 150

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 942 B

View File

@@ -0,0 +1,14 @@
{
"version": 1,
"size": {
"x": 32,
"y": 32
},
"license": "CC0-1.0",
"copyright": "Created by EmoGarbage",
"states": [
{
"name": "icon"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 609 B

View File

@@ -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
]
]
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 615 B

View File

@@ -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"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 B

View File

@@ -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
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 663 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 658 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View 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
]
]
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 454 B

View File

@@ -23,6 +23,9 @@
]
]
},
{
"name": "anomaly2"
},
{
"name": "armory",
"delays": [