diff --git a/Content.Client/Anomaly/AnomalySystem.cs b/Content.Client/Anomaly/AnomalySystem.cs new file mode 100644 index 0000000000..3f42a3e5f7 --- /dev/null +++ b/Content.Client/Anomaly/AnomalySystem.cs @@ -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!; + + /// + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(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(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()) + { + 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); + } + } + } +} diff --git a/Content.Client/Anomaly/Effects/GravityAnomalySystem.cs b/Content.Client/Anomaly/Effects/GravityAnomalySystem.cs new file mode 100644 index 0000000000..444e57b3f4 --- /dev/null +++ b/Content.Client/Anomaly/Effects/GravityAnomalySystem.cs @@ -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 +} diff --git a/Content.Client/Anomaly/Ui/AnomalyGeneratorBoundUserInterface.cs b/Content.Client/Anomaly/Ui/AnomalyGeneratorBoundUserInterface.cs new file mode 100644 index 0000000000..9d8923a2e1 --- /dev/null +++ b/Content.Client/Anomaly/Ui/AnomalyGeneratorBoundUserInterface.cs @@ -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)); + } +} + diff --git a/Content.Client/Anomaly/Ui/AnomalyGeneratorWindow.xaml b/Content.Client/Anomaly/Ui/AnomalyGeneratorWindow.xaml new file mode 100644 index 0000000000..c818d263f6 --- /dev/null +++ b/Content.Client/Anomaly/Ui/AnomalyGeneratorWindow.xaml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + diff --git a/Content.Client/Anomaly/Ui/AnomalyGeneratorWindow.xaml.cs b/Content.Client/Anomaly/Ui/AnomalyGeneratorWindow.xaml.cs new file mode 100644 index 0000000000..cf46f0f8fd --- /dev/null +++ b/Content.Client/Anomaly/Ui/AnomalyGeneratorWindow.xaml.cs @@ -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(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(); + } +} + diff --git a/Content.Client/Anomaly/Ui/AnomalyScannerBoundUserInterface.cs b/Content.Client/Anomaly/Ui/AnomalyScannerBoundUserInterface.cs new file mode 100644 index 0000000000..64f66818eb --- /dev/null +++ b/Content.Client/Anomaly/Ui/AnomalyScannerBoundUserInterface.cs @@ -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(); + } +} + diff --git a/Content.Client/Anomaly/Ui/AnomalyScannerMenu.xaml b/Content.Client/Anomaly/Ui/AnomalyScannerMenu.xaml new file mode 100644 index 0000000000..ac4adf7e0e --- /dev/null +++ b/Content.Client/Anomaly/Ui/AnomalyScannerMenu.xaml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/Content.Client/Anomaly/Ui/AnomalyScannerMenu.xaml.cs b/Content.Client/Anomaly/Ui/AnomalyScannerMenu.xaml.cs new file mode 100644 index 0000000000..14726952d1 --- /dev/null +++ b/Content.Client/Anomaly/Ui/AnomalyScannerMenu.xaml.cs @@ -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(); + } +} diff --git a/Content.Server/Anomaly/AnomalySystem.Generator.cs b/Content.Server/Anomaly/AnomalySystem.Generator.cs new file mode 100644 index 0000000000..cd37280270 --- /dev/null +++ b/Content.Server/Anomaly/AnomalySystem.Generator.cs @@ -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; + +/// +/// This handles anomalous vessel as well as +/// the calculations for how many points they +/// should produce. +/// +public sealed partial class AnomalySystem +{ + /// + /// 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. + /// + public const float GridBoundsMultiplier = 0.6f; + + private void InitializeGenerator() + { + SubscribeLocalEvent(OnGeneratorBUIOpened); + SubscribeLocalEvent(OnGeneratorMaterialAmountChanged); + SubscribeLocalEvent(OnGenerateButtonPressed); + SubscribeLocalEvent(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(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); + } +} diff --git a/Content.Server/Anomaly/AnomalySystem.Scanner.cs b/Content.Server/Anomaly/AnomalySystem.Scanner.cs new file mode 100644 index 0000000000..8c70810682 --- /dev/null +++ b/Content.Server/Anomaly/AnomalySystem.Scanner.cs @@ -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; + +/// +/// This handles the anomaly scanner and it's UI updates. +/// +public sealed partial class AnomalySystem +{ + private void InitializeScanner() + { + SubscribeLocalEvent(OnScannerUiOpened); + SubscribeLocalEvent(OnScannerAfterInteract); + SubscribeLocalEvent(OnScannerDoAfterFinished); + SubscribeLocalEvent(OnScannerDoAfterCancelled); + + SubscribeLocalEvent(OnScannerAnomalyShutdown); + SubscribeLocalEvent(OnScannerAnomalySeverityChanged); + SubscribeLocalEvent(OnScannerAnomalyStabilityChanged); + SubscribeLocalEvent(OnScannerAnomalyHealthChanged); + } + + private void OnScannerAnomalyShutdown(ref AnomalyShutdownEvent args) + { + foreach (var component in EntityQuery()) + { + if (component.ScannedAnomaly != args.Anomaly) + continue; + _ui.TryCloseAll(component.Owner, AnomalyScannerUiKey.Key); + } + } + + private void OnScannerAnomalySeverityChanged(ref AnomalySeverityChangedEvent args) + { + foreach (var component in EntityQuery()) + { + if (component.ScannedAnomaly != args.Anomaly) + continue; + UpdateScannerUi(component.Owner, component); + } + } + + private void OnScannerAnomalyStabilityChanged(ref AnomalyStabilityChangedEvent args) + { + foreach (var component in EntityQuery()) + { + if (component.ScannedAnomaly != args.Anomaly) + continue; + UpdateScannerUi(component.Owner, component); + } + } + + private void OnScannerAnomalyHealthChanged(ref AnomalyHealthChangedEvent args) + { + foreach (var component in EntityQuery()) + { + 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(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(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(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(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; + } +} diff --git a/Content.Server/Anomaly/AnomalySystem.Vessel.cs b/Content.Server/Anomaly/AnomalySystem.Vessel.cs new file mode 100644 index 0000000000..dabd2528fb --- /dev/null +++ b/Content.Server/Anomaly/AnomalySystem.Vessel.cs @@ -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; + +/// +/// This handles anomalous vessel as well as +/// the calculations for how many points they +/// should produce. +/// +public sealed partial class AnomalySystem +{ + private void InitializeVessel() + { + SubscribeLocalEvent(OnVesselShutdown); + SubscribeLocalEvent(OnVesselMapInit); + SubscribeLocalEvent(OnRefreshParts); + SubscribeLocalEvent(OnVesselInteractUsing); + SubscribeLocalEvent(OnExamined); + SubscribeLocalEvent(OnVesselGetPointsPerSecond); + SubscribeLocalEvent(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(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(args.Used, out var scanner) || + scanner.ScannedAnomaly is not {} anomaly) + { + return; + } + + if (!TryComp(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()) + { + var ent = component.Owner; + + if (args.Anomaly != component.Anomaly) + continue; + + component.Anomaly = null; + UpdateVesselAppearance(ent, component); + + if (!args.Supercritical) + continue; + _explosion.TriggerExplosive(ent); + } + } + + /// + /// Updates the appearance of an anomaly vessel + /// based on whether or not it has an anomaly + /// + /// + /// + 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(uid, out var pointLightComponent)) + { + pointLightComponent.Enabled = on; + } + _ambient.SetAmbience(uid, on); + } +} diff --git a/Content.Server/Anomaly/AnomalySystem.cs b/Content.Server/Anomaly/AnomalySystem.cs new file mode 100644 index 0000000000..ed31ab7510 --- /dev/null +++ b/Content.Server/Anomaly/AnomalySystem.cs @@ -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; + +/// +/// This handles logic and interactions relating to +/// +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; + + /// + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnShutdown); + SubscribeLocalEvent(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.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(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); + } + } + + /// + /// Gets the amount of research points generated per second for an anomaly. + /// + /// + /// + /// The amount of points + 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); + } + + /// + /// Gets the localized name of a particle. + /// + /// + /// + 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() + }; + } +} diff --git a/Content.Server/Anomaly/Components/AnomalousParticleComponent.cs b/Content.Server/Anomaly/Components/AnomalousParticleComponent.cs new file mode 100644 index 0000000000..195fe5a941 --- /dev/null +++ b/Content.Server/Anomaly/Components/AnomalousParticleComponent.cs @@ -0,0 +1,23 @@ +using Content.Shared.Anomaly; + +namespace Content.Server.Anomaly.Components; + +/// +/// This is used for projectiles which affect anomalies through colliding with them. +/// +[RegisterComponent] +public sealed class AnomalousParticleComponent : Component +{ + /// + /// The type of particle that the projectile + /// imbues onto the anomaly on contact. + /// + [DataField("particleType", required: true)] + public AnomalousParticleType ParticleType; + + /// + /// The fixture that's checked on collision. + /// + [DataField("fixtureId")] + public string FixtureId = "projectile"; +} diff --git a/Content.Server/Anomaly/Components/AnomalyGeneratorComponent.cs b/Content.Server/Anomaly/Components/AnomalyGeneratorComponent.cs new file mode 100644 index 0000000000..4c9fc39442 --- /dev/null +++ b/Content.Server/Anomaly/Components/AnomalyGeneratorComponent.cs @@ -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; + +/// +/// This is used for a machine that is able to generate +/// anomalies randomly on the station. +/// +[RegisterComponent] +public sealed class AnomalyGeneratorComponent : Component +{ + /// + /// The time at which the cooldown for generating another anomaly will be over + /// + [DataField("cooldownEndTime", customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)] + public TimeSpan CooldownEndTime = TimeSpan.Zero; + + /// + /// The cooldown between generating anomalies. + /// + [DataField("cooldownLength"), ViewVariables(VVAccess.ReadWrite)] + public TimeSpan CooldownLength = TimeSpan.FromMinutes(5); + + /// + /// The material needed to generate an anomaly + /// + [DataField("requiredMaterial", customTypeSerializer: typeof(PrototypeIdSerializer)), ViewVariables(VVAccess.ReadWrite)] + public string RequiredMaterial = "Plasma"; + + /// + /// The amount of material needed to generate a single anomaly + /// + [DataField("materialPerAnomaly"), ViewVariables(VVAccess.ReadWrite)] + public int MaterialPerAnomaly = 1500; // a bit less than a stack of plasma + + /// + /// The random anomaly spawner entity + /// + [DataField("spawnerPrototype", customTypeSerializer: typeof(PrototypeIdSerializer)), ViewVariables(VVAccess.ReadWrite)] + public string SpawnerPrototype = "RandomAnomalySpawner"; +} diff --git a/Content.Server/Anomaly/Components/AnomalyScannerComponent.cs b/Content.Server/Anomaly/Components/AnomalyScannerComponent.cs new file mode 100644 index 0000000000..6fd8c1787d --- /dev/null +++ b/Content.Server/Anomaly/Components/AnomalyScannerComponent.cs @@ -0,0 +1,49 @@ +using System.Threading; +using Robust.Shared.Audio; + +namespace Content.Server.Anomaly.Components; + +/// +/// This is used for scanning anomalies and +/// displaying information about them in the ui +/// +[RegisterComponent] +public sealed class AnomalyScannerComponent : Component +{ + /// + /// The anomaly that was last scanned by this scanner. + /// + [ViewVariables] + public EntityUid? ScannedAnomaly; + + /// + /// How long the scan takes + /// + [DataField("scanDoAfterDuration")] + public float ScanDoAfterDuration = 5; + + public CancellationTokenSource? TokenSource; + + /// + /// The sound plays when the scan finished + /// + [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 +{ +} diff --git a/Content.Server/Anomaly/Components/AnomalyVesselComponent.cs b/Content.Server/Anomaly/Components/AnomalyVesselComponent.cs new file mode 100644 index 0000000000..c833cf636b --- /dev/null +++ b/Content.Server/Anomaly/Components/AnomalyVesselComponent.cs @@ -0,0 +1,40 @@ +using Content.Shared.Construction.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Server.Anomaly.Components; + +/// +/// 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. +/// +[RegisterComponent] +public sealed class AnomalyVesselComponent : Component +{ + /// + /// The anomaly that the vessel is storing. + /// Can be null. + /// + [ViewVariables] + public EntityUid? Anomaly; + + /// + /// A multiplier applied to the amount of points generated. + /// + [ViewVariables(VVAccess.ReadWrite)] + public float PointMultiplier = 1; + + /// + /// The machine part that affects the point multiplier of the vessel + /// + [DataField("machinePartPointModifier", customTypeSerializer: typeof(PrototypeIdSerializer))] + public string MachinePartPointModifier = "ScanningModule"; + + /// + /// A value used to scale the point multiplier + /// with the corresponding part rating. + /// + [DataField("partRatingPointModifier")] + public float PartRatingPointModifier = 1.5f; +} diff --git a/Content.Server/Anomaly/Effects/ElectricityAnomalySystem.cs b/Content.Server/Anomaly/Effects/ElectricityAnomalySystem.cs new file mode 100644 index 0000000000..55c7b1bfa5 --- /dev/null +++ b/Content.Server/Anomaly/Effects/ElectricityAnomalySystem.cs @@ -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!; + + /// + public override void Initialize() + { + SubscribeLocalEvent(OnPulse); + SubscribeLocalEvent(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(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(); + var mobQuery = GetEntityQuery(); + var validEnts = new HashSet(); + 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); + } + } +} diff --git a/Content.Server/Anomaly/Effects/GravityAnomalySystem.cs b/Content.Server/Anomaly/Effects/GravityAnomalySystem.cs new file mode 100644 index 0000000000..a928add35b --- /dev/null +++ b/Content.Server/Anomaly/Effects/GravityAnomalySystem.cs @@ -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; + +/// +/// This handles logic and events relating to and +/// +public sealed class GravityAnomalySystem : SharedGravityAnomalySystem +{ + /// + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnSeverityChanged); + SubscribeLocalEvent(OnStabilityChanged); + } + + private void OnSeverityChanged(EntityUid uid, GravityAnomalyComponent component, ref AnomalySeverityChangedEvent args) + { + if (TryComp(uid, out var radSource)) + radSource.Intensity = component.MaxRadiationIntensity * args.Severity; + + if (!TryComp(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(uid, out var gravityWell)) + gravityWell.MaxRange = component.MaxGravityWellRange * args.Stability; + } +} diff --git a/Content.Server/Anomaly/Effects/PyroclasticAnomalySystem.cs b/Content.Server/Anomaly/Effects/PyroclasticAnomalySystem.cs new file mode 100644 index 0000000000..87508802a4 --- /dev/null +++ b/Content.Server/Anomaly/Effects/PyroclasticAnomalySystem.cs @@ -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; + +/// +/// This handles and the events from +/// +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!; + + /// + public override void Initialize() + { + SubscribeLocalEvent(OnPulse); + SubscribeLocalEvent(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()) + { + 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(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); + } + } +} diff --git a/Content.Server/Beam/BeamSystem.cs b/Content.Server/Beam/BeamSystem.cs index dd2fb798b8..0f0cf6fac3 100644 --- a/Content.Server/Beam/BeamSystem.cs +++ b/Content.Server/Beam/BeamSystem.cs @@ -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(ent, out var physics) && TryComp(ent, out var beam)) + if (!TryComp(ent, out var physics) || !TryComp(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); } /// diff --git a/Content.Server/Singularity/EntitySystems/EmitterSystem.cs b/Content.Server/Singularity/EntitySystems/EmitterSystem.cs index 44986ab518..6393df7ee6 100644 --- a/Content.Server/Singularity/EntitySystems/EmitterSystem.cs +++ b/Content.Server/Singularity/EntitySystems/EmitterSystem.cs @@ -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(ReceivedChanged); + SubscribeLocalEvent(OnApcChanged); SubscribeLocalEvent(OnInteractHand); + SubscribeLocalEvent>(OnGetVerb); + SubscribeLocalEvent(OnExamined); SubscribeLocalEvent(OnRefreshParts); SubscribeLocalEvent(OnUpgradeExamine); + SubscribeLocalEvent(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 args) + { + if (!args.CanAccess || !args.CanInteract || args.Hands == null) + return; + + if (TryComp(uid, out var lockComp) && lockComp.Locked) + return; + + if (component.SelectableTypes.Count < 2) + return; + + foreach (var type in component.SelectableTypes) + { + var proto = _prototype.Index(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(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(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(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(component.Owner, out var powerConsumer)) powerConsumer.DrawRate = component.PowerUseActive; + if (TryComp(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(component.Owner, out var powerConsumer) && - (powerConsumer.DrawRate <= powerConsumer.ReceivedPower || - MathHelper.CloseTo(powerConsumer.DrawRate, powerConsumer.ReceivedPower, 0.0001f))); Fire(component); diff --git a/Content.Shared.Database/LogType.cs b/Content.Shared.Database/LogType.cs index 77d77747e4..ad6e6ac2e9 100644 --- a/Content.Shared.Database/LogType.cs +++ b/Content.Shared.Database/LogType.cs @@ -81,4 +81,5 @@ public enum LogType Stamina = 76, EntitySpawn = 77, AdminMessage = 78, + Anomaly = 79 } diff --git a/Content.Shared/Anomaly/Components/AnomalyComponent.cs b/Content.Shared/Anomaly/Components/AnomalyComponent.cs new file mode 100644 index 0000000000..d19616323d --- /dev/null +++ b/Content.Shared/Anomaly/Components/AnomalyComponent.cs @@ -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; + +/// +/// 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 +/// +[RegisterComponent, NetworkedComponent] +public sealed class AnomalyComponent : Component +{ + /// + /// 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. + /// + /// + /// Note that this doesn't refer to stability as a percentage: This is an arbitrary + /// value that only matters in relation to the and + /// + [ViewVariables(VVAccess.ReadWrite)] + public float Stability = 0f; + + /// + /// 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. + /// + /// + /// Wacky-Stability scale lives on in my heart. - emo + /// + [ViewVariables(VVAccess.ReadWrite)] + public float Severity = 0f; + + #region Health + /// + /// 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. + /// + [ViewVariables(VVAccess.ReadWrite)] + public float Health = 1f; + + /// + /// If the of the anomaly exceeds this value, it + /// becomes too unstable to support itself and starts decreasing in . + /// + [DataField("decayhreshold"), ViewVariables(VVAccess.ReadWrite)] + public float DecayThreshold = 0.15f; + + /// + /// The amount of health lost when the stability is below the + /// + [DataField("healthChangePerSecond"), ViewVariables(VVAccess.ReadWrite)] + public float HealthChangePerSecond = -0.05f; + #endregion + + #region Growth + /// + /// If the of the anomaly exceeds this value, it + /// becomes unstable and starts increasing in . + /// + [DataField("growthThreshold"), ViewVariables(VVAccess.ReadWrite)] + public float GrowthThreshold = 0.5f; + + /// + /// A coefficient used for calculating the increase in severity when above the GrowthThreshold + /// + [DataField("severityGrowthCoefficient"), ViewVariables(VVAccess.ReadWrite)] + public float SeverityGrowthCoefficient = 0.07f; + #endregion + + #region Pulse + /// + /// The time at which the next artifact pulse will occur. + /// + [DataField("nextPulseTime", customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)] + public TimeSpan NextPulseTime = TimeSpan.MaxValue; + + /// + /// The minimum interval between pulses. + /// + [DataField("minPulseLength")] + public TimeSpan MinPulseLength = TimeSpan.FromMinutes(1); + + /// + /// The maximum interval between pulses. + /// + [DataField("maxPulseLength")] + public TimeSpan MaxPulseLength = TimeSpan.FromMinutes(2); + + /// + /// A percentage by which the length of a pulse might vary. + /// + [DataField("pulseVariation")] + public float PulseVariation = .1f; + + /// + /// The sound played when an anomaly pulses + /// + [DataField("pulseSound")] + public SoundSpecifier? PulseSound = new SoundCollectionSpecifier("RadiationPulse"); + + /// + /// The sound plays when an anomaly goes supercritical + /// + [DataField("supercriticalSound")] + public SoundSpecifier? SupercriticalSound = new SoundCollectionSpecifier("explosion"); + #endregion + + /// + /// The range of initial values for stability + /// + /// + /// +/- 0.2 from perfect stability (0.5) + /// + [DataField("initialStabilityRange")] + public (float, float) InitialStabilityRange = (0.4f, 0.6f); + + /// + /// The range of initial values for severity + /// + /// + /// Between 0 and 0.5, which should be all mild effects + /// + [DataField("initialSeverityRange")] + public (float, float) InitialSeverityRange = (0.1f, 0.5f); + + /// + /// The particle type that increases the severity of the anomaly. + /// + [DataField("severityParticleType")] + public AnomalousParticleType SeverityParticleType; + + /// + /// The amount that the increases by when hit + /// of an anomalous particle of . + /// + [DataField("severityPerSeverityHit")] + public float SeverityPerSeverityHit = 0.025f; + + /// + /// The particle type that destabilizes the anomaly. + /// + [DataField("destabilizingParticleType")] + public AnomalousParticleType DestabilizingParticleType; + + /// + /// The amount that the increases by when hit + /// of an anomalous particle of . + /// + [DataField("stabilityPerDestabilizingHit")] + public float StabilityPerDestabilizingHit = 0.04f; + + /// + /// The particle type that weakens the anomalys health. + /// + [DataField("weakeningParticleType")] + public AnomalousParticleType WeakeningParticleType; + + /// + /// The amount that the increases by when hit + /// of an anomalous particle of . + /// + [DataField("healthPerWeakeningeHit")] + public float HealthPerWeakeningeHit = -0.05f; + + /// + /// The amount that the increases by when hit + /// of an anomalous particle of . + /// + [DataField("stabilityPerWeakeningeHit")] + public float StabilityPerWeakeningeHit = -0.02f; + + #region Points and Vessels + /// + /// The vessel that the anomaly is connceted to. Stored so that multiple + /// vessels cannot connect to the same anomaly. + /// + [ViewVariables(VVAccess.ReadWrite)] + public EntityUid? ConnectedVessel; + + /// + /// The minimum amount of research points generated per second + /// + [DataField("minPointsPerSecond")] + public int MinPointsPerSecond; + + /// + /// The maximum amount of research points generated per second + /// This doesn't include the point bonus for being unstable. + /// + [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; + } +} + +/// +/// Event raised at regular intervals on an anomaly to do whatever its effect is. +/// +/// +/// +[ByRefEvent] +public readonly record struct AnomalyPulseEvent(float Stabiltiy, float Severity) +{ + public readonly float Stabiltiy = Stabiltiy; + public readonly float Severity = Severity; +} + +/// +/// Event raised on an anomaly when it reaches a supercritical point. +/// +[ByRefEvent] +public readonly record struct AnomalySupercriticalEvent; + +/// +/// Event broadcast after an anomaly goes supercritical +/// +/// The anomaly being shut down. +/// Whether or not the anomaly shut down passively or via a supercritical event. +[ByRefEvent] +public readonly record struct AnomalyShutdownEvent(EntityUid Anomaly, bool Supercritical); + +/// +/// Event broadcast when an anomaly's severity is changed. +/// +/// The anomaly being changed +[ByRefEvent] +public readonly record struct AnomalySeverityChangedEvent(EntityUid Anomaly, float Severity); + +/// +/// Event broadcast when an anomaly's stability is changed. +/// +[ByRefEvent] +public readonly record struct AnomalyStabilityChangedEvent(EntityUid Anomaly, float Stability); + +/// +/// Event broadcast when an anomaly's health is changed. +/// +/// The anomaly being changed +[ByRefEvent] +public readonly record struct AnomalyHealthChangedEvent(EntityUid Anomaly, float Health); diff --git a/Content.Shared/Anomaly/Components/AnomalyPulsingComponent.cs b/Content.Shared/Anomaly/Components/AnomalyPulsingComponent.cs new file mode 100644 index 0000000000..9c12ec11c8 --- /dev/null +++ b/Content.Shared/Anomaly/Components/AnomalyPulsingComponent.cs @@ -0,0 +1,22 @@ +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Shared.Anomaly.Components; + +/// +/// This component tracks anomalies that are currently pulsing +/// +[RegisterComponent] +public sealed class AnomalyPulsingComponent : Component +{ + /// + /// The time at which the pulse will be over. + /// + [DataField("endTime", customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)] + public TimeSpan EndTime = TimeSpan.MaxValue; + + /// + /// How long the pulse visual lasts + /// + [ViewVariables(VVAccess.ReadWrite)] + public TimeSpan PulseDuration = TimeSpan.FromSeconds(5); +} diff --git a/Content.Shared/Anomaly/Components/AnomalySupercriticalComponent.cs b/Content.Shared/Anomaly/Components/AnomalySupercriticalComponent.cs new file mode 100644 index 0000000000..bc5bb381ef --- /dev/null +++ b/Content.Shared/Anomaly/Components/AnomalySupercriticalComponent.cs @@ -0,0 +1,37 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Serialization; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Shared.Anomaly.Components; + +/// +/// Tracks anomalies going supercritical +/// +[RegisterComponent, NetworkedComponent] +public sealed class AnomalySupercriticalComponent : Component +{ + /// + /// The time when the supercritical animation ends and it does whatever effect. + /// + [DataField("endTime", customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)] + public TimeSpan EndTime = TimeSpan.MaxValue; + + /// + /// The length of the animation before it goes supercritical. + /// + [ViewVariables(VVAccess.ReadWrite)] + public TimeSpan SupercriticalDuration = TimeSpan.FromSeconds(10); + + /// + /// The maximum size the anomaly scales to while going supercritical + /// + [DataField("maxScaleAmount")] + public float MaxScaleAmount = 3; +} + +[Serializable, NetSerializable] +public sealed class AnomalySupercriticalComponentState : ComponentState +{ + public TimeSpan EndTime; + public TimeSpan Duration; +} diff --git a/Content.Shared/Anomaly/Effects/Components/ElectricityAnomalyComponent.cs b/Content.Shared/Anomaly/Effects/Components/ElectricityAnomalyComponent.cs new file mode 100644 index 0000000000..fc430127ef --- /dev/null +++ b/Content.Shared/Anomaly/Effects/Components/ElectricityAnomalyComponent.cs @@ -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); +} diff --git a/Content.Shared/Anomaly/Effects/Components/GravityAnomalyComponent.cs b/Content.Shared/Anomaly/Effects/Components/GravityAnomalyComponent.cs new file mode 100644 index 0000000000..a6f80aeda0 --- /dev/null +++ b/Content.Shared/Anomaly/Effects/Components/GravityAnomalyComponent.cs @@ -0,0 +1,53 @@ +namespace Content.Shared.Anomaly.Effects.Components; + +[RegisterComponent] +public sealed class GravityAnomalyComponent : Component +{ + /// + /// The maximumum size the GravityWellComponent MaxRange can be. + /// Is scaled linearly with stability. + /// + [DataField("maxGravityWellRange"), ViewVariables(VVAccess.ReadWrite)] + public float MaxGravityWellRange = 8f; + + /// + /// The maximum distance from which the anomaly + /// can throw you via a pulse. + /// + [DataField("maxThrowRange"), ViewVariables(VVAccess.ReadWrite)] + public float MaxThrowRange = 5f; + + /// + /// The maximum strength the anomaly + /// can throw you via a pulse + /// + [DataField("maxThrowStrength"), ViewVariables(VVAccess.ReadWrite)] + public float MaxThrowStrength = 10; + + /// + /// The maximum Intensity of the RadiationSourceComponent. + /// Is scaled linearly with stability. + /// + [DataField("maxRadiationIntensity"), ViewVariables(VVAccess.ReadWrite)] + public float MaxRadiationIntensity = 3f; + + /// + /// The minimum acceleration value for GravityWellComponent + /// Is scaled linearly with stability. + /// + [DataField("minAccel"), ViewVariables(VVAccess.ReadWrite)] + public float MinAccel = 1f; + + /// + /// The maximum acceleration value for GravityWellComponent + /// Is scaled linearly with stability. + /// + [DataField("maxAccel"), ViewVariables(VVAccess.ReadWrite)] + public float MaxAccel = 5f; + + /// + /// The range around the anomaly that will be spaced on supercritical. + /// + [DataField("spaceRange"), ViewVariables(VVAccess.ReadWrite)] + public float SpaceRange = 3f; +} diff --git a/Content.Shared/Anomaly/Effects/Components/PyroclasticAnomalyComponent.cs b/Content.Shared/Anomaly/Effects/Components/PyroclasticAnomalyComponent.cs new file mode 100644 index 0000000000..5f3c1c2595 --- /dev/null +++ b/Content.Shared/Anomaly/Effects/Components/PyroclasticAnomalyComponent.cs @@ -0,0 +1,54 @@ +using Content.Shared.Atmos; + +namespace Content.Shared.Anomaly.Effects.Components; + +[RegisterComponent] +public sealed class PyroclasticAnomalyComponent : Component +{ + /// + /// The MAXIMUM amount of heat released per second. + /// This is scaled linearly with the Severity of the anomaly. + /// + /// + /// I have no clue if this is balanced. + /// + [DataField("heatPerSecond")] + public float HeatPerSecond = 50; + + /// + /// The maximum distance from which you can be ignited by the anomaly. + /// + [DataField("maximumIgnitionRadius")] + public float MaximumIgnitionRadius = 8f; + + /// + /// The minimum amount of severity required + /// before the anomaly becomes a hotspot. + /// + [DataField("anomalyHotspotThreshold")] + public float AnomalyHotspotThreshold = 0.6f; + + /// + /// The temperature of the hotspot where the anomaly is + /// + [DataField("hotspotExposeTemperature")] + public float HotspotExposeTemperature = 1000; + + /// + /// The volume of the hotspot where the anomaly is. + /// + [DataField("hotspotExposeVolume")] + public float HotspotExposeVolume = 50; + + /// + /// Gas released when the anomaly goes supercritical. + /// + [DataField("supercriticalGas")] + public Gas SupercriticalGas = Gas.Plasma; + + /// + /// The amount of gas released when the anomaly goes supercritical + /// + [DataField("supercriticalMoleAmount")] + public float SupercriticalMoleAmount = 50f; +} diff --git a/Content.Shared/Anomaly/Effects/SharedGravityAnomalySystem.cs b/Content.Shared/Anomaly/Effects/SharedGravityAnomalySystem.cs new file mode 100644 index 0000000000..8cb1714afb --- /dev/null +++ b/Content.Shared/Anomaly/Effects/SharedGravityAnomalySystem.cs @@ -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!; + + /// + public override void Initialize() + { + SubscribeLocalEvent(OnAnomalyPulse); + SubscribeLocalEvent(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); + } + } +} + diff --git a/Content.Shared/Anomaly/SharedAnomaly.cs b/Content.Shared/Anomaly/SharedAnomaly.cs new file mode 100644 index 0000000000..fe93cff117 --- /dev/null +++ b/Content.Shared/Anomaly/SharedAnomaly.cs @@ -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 +} + +/// +/// The types of anomalous particles used +/// for interfacing with anomalies. +/// +/// +/// 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. +/// +[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 +{ + +} diff --git a/Content.Shared/Anomaly/SharedAnomalySystem.cs b/Content.Shared/Anomaly/SharedAnomalySystem.cs new file mode 100644 index 0000000000..6d954dcc82 --- /dev/null +++ b/Content.Shared/Anomaly/SharedAnomalySystem.cs @@ -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(OnAnomalyGetState); + SubscribeLocalEvent(OnAnomalyHandleState); + SubscribeLocalEvent(OnSupercriticalGetState); + SubscribeLocalEvent(OnSupercriticalHandleState); + + SubscribeLocalEvent(OnAnomalyUnpause); + SubscribeLocalEvent(OnPulsingUnpause); + SubscribeLocalEvent(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(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); + } + + /// + /// Begins the animation for going supercritical + /// + /// + public void StartSupercriticalEvent(EntityUid uid) + { + // don't restart it if it's already begun + if (HasComp(uid)) + return; + + Log.Add(LogType.Anomaly, LogImpact.High, $"Anomaly {ToPrettyString(uid)} began to go supercritical."); + + var super = EnsureComp(uid); + super.EndTime = Timing.CurTime + super.SupercriticalDuration; + Appearance.SetData(uid, AnomalyVisuals.Supercritical, true); + Dirty(super); + } + + /// + /// 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 + /// + /// + /// + 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); + } + + /// + /// Ends an anomaly, cleaning up all entities that may be associated with it. + /// + /// The anomaly being shut down + /// + /// Whether or not the anomaly ended via supercritical event + 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); + } + + /// + /// Changes the stability of the anomaly. + /// + /// + /// + /// + 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); + } + + /// + /// Changes the severity of an anomaly, going supercritical if it exceeds 1. + /// + /// + /// + /// + 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); + } + + /// + /// Changes the health of an anomaly, ending it if it's less than 0. + /// + /// + /// + /// + 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); + } + + /// + /// Gets the length of time between each pulse + /// for an anomaly based on its current stability. + /// + /// + /// 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. + /// + /// + /// The length of time as a TimeSpan, not including random variation. + 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; + } + + /// + /// Gets the increase in an anomaly's severity due + /// to being above its growth threshold + /// + /// + /// The increase in severity for this anomaly + 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()) + { + 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()) + { + var ent = pulse.Owner; + + if (Timing.CurTime > pulse.EndTime) + { + Appearance.SetData(ent, AnomalyVisuals.IsPulsing, false); + RemComp(ent, pulse); + } + } + + foreach (var (super, anom) in EntityQuery()) + { + var ent = anom.Owner; + + if (Timing.CurTime <= super.EndTime) + continue; + DoAnomalySupercriticalEvent(ent, anom); + RemComp(ent, super); + } + } +} diff --git a/Content.Shared/Singularity/Components/SharedEmitterComponent.cs b/Content.Shared/Singularity/Components/SharedEmitterComponent.cs index f2362cae85..7579823246 100644 --- a/Content.Shared/Singularity/Components/SharedEmitterComponent.cs +++ b/Content.Shared/Singularity/Components/SharedEmitterComponent.cs @@ -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 /// /// The entity that is spawned when the emitter fires. /// - [DataField("boltType")] + [DataField("boltType", customTypeSerializer: typeof(PrototypeIdSerializer))] public string BoltType = "EmitterBolt"; + [DataField("selectableTypes", customTypeSerializer: typeof(PrototypeIdListSerializer))] + public List SelectableTypes = new(); + /// /// The current amount of power being used. /// diff --git a/Content.Shared/Verbs/VerbCategory.cs b/Content.Shared/Verbs/VerbCategory.cs index 1f63f3d106..63a1cbced6 100644 --- a/Content.Shared/Verbs/VerbCategory.cs +++ b/Content.Shared/Verbs/VerbCategory.cs @@ -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); } } diff --git a/Resources/Audio/Ambience/Objects/anomaly_generator.ogg b/Resources/Audio/Ambience/Objects/anomaly_generator.ogg new file mode 100644 index 0000000000..7b28e732a5 Binary files /dev/null and b/Resources/Audio/Ambience/Objects/anomaly_generator.ogg differ diff --git a/Resources/Audio/Ambience/Objects/attributions.yml b/Resources/Audio/Ambience/Objects/attributions.yml new file mode 100644 index 0000000000..8322c91a48 --- /dev/null +++ b/Resources/Audio/Ambience/Objects/attributions.yml @@ -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/" \ No newline at end of file diff --git a/Resources/Audio/Ambience/anomaly_drone.ogg b/Resources/Audio/Ambience/anomaly_drone.ogg new file mode 100644 index 0000000000..5e9d4f8247 Binary files /dev/null and b/Resources/Audio/Ambience/anomaly_drone.ogg differ diff --git a/Resources/Audio/Ambience/attributions.yml b/Resources/Audio/Ambience/attributions.yml new file mode 100644 index 0000000000..e6979603be --- /dev/null +++ b/Resources/Audio/Ambience/attributions.yml @@ -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/" \ No newline at end of file diff --git a/Resources/Audio/Items/attributions.yml b/Resources/Audio/Items/attributions.yml index 92bce1cd2d..aee30635ab 100644 --- a/Resources/Audio/Items/attributions.yml +++ b/Resources/Audio/Items/attributions.yml @@ -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" diff --git a/Resources/Audio/Items/beep.ogg b/Resources/Audio/Items/beep.ogg new file mode 100644 index 0000000000..65abe4208c Binary files /dev/null and b/Resources/Audio/Items/beep.ogg differ diff --git a/Resources/Locale/en-US/anomaly/anomaly.ftl b/Resources/Locale/en-US/anomaly/anomaly.ftl new file mode 100644 index 0000000000..725742ece6 --- /dev/null +++ b/Resources/Locale/en-US/anomaly/anomaly.ftl @@ -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 \ No newline at end of file diff --git a/Resources/Locale/en-US/prototypes/catalog/research/technologies.ftl b/Resources/Locale/en-US/prototypes/catalog/research/technologies.ftl index d38c715637..5a2ec20e00 100644 --- a/Resources/Locale/en-US/prototypes/catalog/research/technologies.ftl +++ b/Resources/Locale/en-US/prototypes/catalog/research/technologies.ftl @@ -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. diff --git a/Resources/Locale/en-US/singularity/components/emitter-component.ftl b/Resources/Locale/en-US/singularity/components/emitter-component.ftl index 2c316e1d6b..7c1b33d96b 100644 --- a/Resources/Locale/en-US/singularity/components/emitter-component.ftl +++ b/Resources/Locale/en-US/singularity/components/emitter-component.ftl @@ -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} \ No newline at end of file diff --git a/Resources/Locale/en-US/verbs/verb-system.ftl b/Resources/Locale/en-US/verbs/verb-system.ftl index 32a3f5d521..2bebddca61 100644 --- a/Resources/Locale/en-US/verbs/verb-system.ftl +++ b/Resources/Locale/en-US/verbs/verb-system.ftl @@ -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 diff --git a/Resources/Prototypes/Catalog/Fills/Lockers/science.yml b/Resources/Prototypes/Catalog/Fills/Lockers/science.yml index 51c4a69901..518c2d6e76 100644 --- a/Resources/Prototypes/Catalog/Fills/Lockers/science.yml +++ b/Resources/Prototypes/Catalog/Fills/Lockers/science.yml @@ -9,3 +9,9 @@ - id: ClothingHeadsetScience - id: ClothingMaskSterile - id: ClothingOuterCoatLab + - id: AnomalyScanner + prob: 0.5 + orGroup: Scanner + - id: NodeScanner + prob: 0.5 + orGroup: Scanner diff --git a/Resources/Prototypes/Catalog/Research/technologies.yml b/Resources/Prototypes/Catalog/Research/technologies.yml index 32d04c0619..0f6c553661 100644 --- a/Resources/Prototypes/Catalog/Research/technologies.yml +++ b/Resources/Prototypes/Catalog/Research/technologies.yml @@ -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 diff --git a/Resources/Prototypes/Entities/Markers/Spawners/Random/anomaly.yml b/Resources/Prototypes/Entities/Markers/Spawners/Random/anomaly.yml new file mode 100644 index 0000000000..a082fae7d6 --- /dev/null +++ b/Resources/Prototypes/Entities/Markers/Spawners/Random/anomaly.yml @@ -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 diff --git a/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/Machine/production.yml b/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/Machine/production.yml index 9810e1a322..8e8a0800c1 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/Machine/production.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/Machine/production.yml @@ -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 diff --git a/Resources/Prototypes/Entities/Objects/Specific/Research/anomaly.yml b/Resources/Prototypes/Entities/Objects/Specific/Research/anomaly.yml new file mode 100644 index 0000000000..e0d862a705 --- /dev/null +++ b/Resources/Prototypes/Entities/Objects/Specific/Research/anomaly.yml @@ -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 diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/projectiles.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/projectiles.yml index 8101d4246c..0c3ed4708d 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/projectiles.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/projectiles.yml @@ -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 diff --git a/Resources/Prototypes/Entities/Structures/Machines/anomaly_equipment.yml b/Resources/Prototypes/Entities/Structures/Machines/anomaly_equipment.yml new file mode 100644 index 0000000000..cb108d0b73 --- /dev/null +++ b/Resources/Prototypes/Entities/Structures/Machines/anomaly_equipment.yml @@ -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 \ No newline at end of file diff --git a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml index 96162ce15e..ae0374c6c8 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml @@ -169,6 +169,7 @@ - MiningDrill - ConveyorBeltAssembly - AppraisalTool + - AnomalyScanner - RCD - RCDAmmo - HydroponicsToolScythe @@ -314,6 +315,8 @@ - SeedExtractorMachineCircuitboard - AnalysisComputerCircuitboard - ExosuitFabricatorMachineCircuitboard + - AnomalyVesselCircuitboard + - APECircuitboard - ArtifactAnalyzerMachineCircuitboard - TraversalDistorterMachineCircuitboard - BoozeDispenserMachineCircuitboard diff --git a/Resources/Prototypes/Entities/Structures/Specific/anomalies.yml b/Resources/Prototypes/Entities/Structures/Specific/anomalies.yml new file mode 100644 index 0000000000..01dd3dce8b --- /dev/null +++ b/Resources/Prototypes/Entities/Structures/Specific/anomalies.yml @@ -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 \ No newline at end of file diff --git a/Resources/Prototypes/Entities/Structures/Wallmounts/Signs/signs.yml b/Resources/Prototypes/Entities/Structures/Wallmounts/Signs/signs.yml index 149b6d56ef..5fa1afa26b 100644 --- a/Resources/Prototypes/Entities/Structures/Wallmounts/Signs/signs.yml +++ b/Resources/Prototypes/Entities/Structures/Wallmounts/Signs/signs.yml @@ -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 diff --git a/Resources/Prototypes/Recipes/Lathes/devices.yml b/Resources/Prototypes/Recipes/Lathes/devices.yml index bd4f1146f8..135d1005ae 100644 --- a/Resources/Prototypes/Recipes/Lathes/devices.yml +++ b/Resources/Prototypes/Recipes/Lathes/devices.yml @@ -48,3 +48,11 @@ Steel: 100 Plastic: 200 Glass: 100 + +- type: latheRecipe + id: AnomalyScanner + result: AnomalyScanner + completetime: 2 + materials: + Plastic: 200 + Glass: 150 \ No newline at end of file diff --git a/Resources/Prototypes/Recipes/Lathes/electronics.yml b/Resources/Prototypes/Recipes/Lathes/electronics.yml index 19280a620a..265516c149 100644 --- a/Resources/Prototypes/Recipes/Lathes/electronics.yml +++ b/Resources/Prototypes/Recipes/Lathes/electronics.yml @@ -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 diff --git a/Resources/Textures/Objects/Specific/Research/anomalyscanner.rsi/icon.png b/Resources/Textures/Objects/Specific/Research/anomalyscanner.rsi/icon.png new file mode 100644 index 0000000000..31706aeb91 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Research/anomalyscanner.rsi/icon.png differ diff --git a/Resources/Textures/Objects/Specific/Research/anomalyscanner.rsi/meta.json b/Resources/Textures/Objects/Specific/Research/anomalyscanner.rsi/meta.json new file mode 100644 index 0000000000..76eff64a56 --- /dev/null +++ b/Resources/Textures/Objects/Specific/Research/anomalyscanner.rsi/meta.json @@ -0,0 +1,14 @@ +{ + "version": 1, + "size": { + "x": 32, + "y": 32 + }, + "license": "CC0-1.0", + "copyright": "Created by EmoGarbage", + "states": [ + { + "name": "icon" + } + ] +} diff --git a/Resources/Textures/Structures/Machines/Anomaly/anomaly_generator.rsi/base.png b/Resources/Textures/Structures/Machines/Anomaly/anomaly_generator.rsi/base.png new file mode 100644 index 0000000000..b53ee643c1 Binary files /dev/null and b/Resources/Textures/Structures/Machines/Anomaly/anomaly_generator.rsi/base.png differ diff --git a/Resources/Textures/Structures/Machines/Anomaly/anomaly_generator.rsi/inserting.png b/Resources/Textures/Structures/Machines/Anomaly/anomaly_generator.rsi/inserting.png new file mode 100644 index 0000000000..aeb74f2c86 Binary files /dev/null and b/Resources/Textures/Structures/Machines/Anomaly/anomaly_generator.rsi/inserting.png differ diff --git a/Resources/Textures/Structures/Machines/Anomaly/anomaly_generator.rsi/meta.json b/Resources/Textures/Structures/Machines/Anomaly/anomaly_generator.rsi/meta.json new file mode 100644 index 0000000000..cd4793cdd2 --- /dev/null +++ b/Resources/Textures/Structures/Machines/Anomaly/anomaly_generator.rsi/meta.json @@ -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 + ] + ] + } + ] +} diff --git a/Resources/Textures/Structures/Machines/Anomaly/anomaly_generator.rsi/panel.png b/Resources/Textures/Structures/Machines/Anomaly/anomaly_generator.rsi/panel.png new file mode 100644 index 0000000000..234adcd5ee Binary files /dev/null and b/Resources/Textures/Structures/Machines/Anomaly/anomaly_generator.rsi/panel.png differ diff --git a/Resources/Textures/Structures/Machines/Anomaly/anomaly_generator.rsi/unshaded.png b/Resources/Textures/Structures/Machines/Anomaly/anomaly_generator.rsi/unshaded.png new file mode 100644 index 0000000000..6608244157 Binary files /dev/null and b/Resources/Textures/Structures/Machines/Anomaly/anomaly_generator.rsi/unshaded.png differ diff --git a/Resources/Textures/Structures/Machines/Anomaly/anomaly_vessel.rsi/anomaly.png b/Resources/Textures/Structures/Machines/Anomaly/anomaly_vessel.rsi/anomaly.png new file mode 100644 index 0000000000..e649b36f53 Binary files /dev/null and b/Resources/Textures/Structures/Machines/Anomaly/anomaly_vessel.rsi/anomaly.png differ diff --git a/Resources/Textures/Structures/Machines/Anomaly/anomaly_vessel.rsi/base.png b/Resources/Textures/Structures/Machines/Anomaly/anomaly_vessel.rsi/base.png new file mode 100644 index 0000000000..e292496fbe Binary files /dev/null and b/Resources/Textures/Structures/Machines/Anomaly/anomaly_vessel.rsi/base.png differ diff --git a/Resources/Textures/Structures/Machines/Anomaly/anomaly_vessel.rsi/meta.json b/Resources/Textures/Structures/Machines/Anomaly/anomaly_vessel.rsi/meta.json new file mode 100644 index 0000000000..1a9a61933d --- /dev/null +++ b/Resources/Textures/Structures/Machines/Anomaly/anomaly_vessel.rsi/meta.json @@ -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" + } + ] +} diff --git a/Resources/Textures/Structures/Machines/Anomaly/anomaly_vessel.rsi/panel.png b/Resources/Textures/Structures/Machines/Anomaly/anomaly_vessel.rsi/panel.png new file mode 100644 index 0000000000..b350a50415 Binary files /dev/null and b/Resources/Textures/Structures/Machines/Anomaly/anomaly_vessel.rsi/panel.png differ diff --git a/Resources/Textures/Structures/Machines/Anomaly/anomaly_vessel.rsi/powered.png b/Resources/Textures/Structures/Machines/Anomaly/anomaly_vessel.rsi/powered.png new file mode 100644 index 0000000000..2ed947de62 Binary files /dev/null and b/Resources/Textures/Structures/Machines/Anomaly/anomaly_vessel.rsi/powered.png differ diff --git a/Resources/Textures/Structures/Machines/Anomaly/ape.rsi/base.png b/Resources/Textures/Structures/Machines/Anomaly/ape.rsi/base.png new file mode 100644 index 0000000000..86c899fd4c Binary files /dev/null and b/Resources/Textures/Structures/Machines/Anomaly/ape.rsi/base.png differ diff --git a/Resources/Textures/Structures/Machines/Anomaly/ape.rsi/firing.png b/Resources/Textures/Structures/Machines/Anomaly/ape.rsi/firing.png new file mode 100644 index 0000000000..20c993ec39 Binary files /dev/null and b/Resources/Textures/Structures/Machines/Anomaly/ape.rsi/firing.png differ diff --git a/Resources/Textures/Structures/Machines/Anomaly/ape.rsi/locked.png b/Resources/Textures/Structures/Machines/Anomaly/ape.rsi/locked.png new file mode 100644 index 0000000000..fa85ff1d52 Binary files /dev/null and b/Resources/Textures/Structures/Machines/Anomaly/ape.rsi/locked.png differ diff --git a/Resources/Textures/Structures/Machines/Anomaly/ape.rsi/meta.json b/Resources/Textures/Structures/Machines/Anomaly/ape.rsi/meta.json new file mode 100644 index 0000000000..2a826897e4 --- /dev/null +++ b/Resources/Textures/Structures/Machines/Anomaly/ape.rsi/meta.json @@ -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 + } + ] +} diff --git a/Resources/Textures/Structures/Machines/Anomaly/ape.rsi/panel.png b/Resources/Textures/Structures/Machines/Anomaly/ape.rsi/panel.png new file mode 100644 index 0000000000..dd170d14cd Binary files /dev/null and b/Resources/Textures/Structures/Machines/Anomaly/ape.rsi/panel.png differ diff --git a/Resources/Textures/Structures/Machines/Anomaly/ape.rsi/underpowered.png b/Resources/Textures/Structures/Machines/Anomaly/ape.rsi/underpowered.png new file mode 100644 index 0000000000..9cc2d9d5a2 Binary files /dev/null and b/Resources/Textures/Structures/Machines/Anomaly/ape.rsi/underpowered.png differ diff --git a/Resources/Textures/Structures/Machines/Anomaly/ape.rsi/unshaded.png b/Resources/Textures/Structures/Machines/Anomaly/ape.rsi/unshaded.png new file mode 100644 index 0000000000..b5fb9bb69e Binary files /dev/null and b/Resources/Textures/Structures/Machines/Anomaly/ape.rsi/unshaded.png differ diff --git a/Resources/Textures/Structures/Specific/anomaly.rsi/anom1-pulse.png b/Resources/Textures/Structures/Specific/anomaly.rsi/anom1-pulse.png new file mode 100644 index 0000000000..3ea4f894bf Binary files /dev/null and b/Resources/Textures/Structures/Specific/anomaly.rsi/anom1-pulse.png differ diff --git a/Resources/Textures/Structures/Specific/anomaly.rsi/anom1.png b/Resources/Textures/Structures/Specific/anomaly.rsi/anom1.png new file mode 100644 index 0000000000..0075a07a51 Binary files /dev/null and b/Resources/Textures/Structures/Specific/anomaly.rsi/anom1.png differ diff --git a/Resources/Textures/Structures/Specific/anomaly.rsi/anom2-pulse.png b/Resources/Textures/Structures/Specific/anomaly.rsi/anom2-pulse.png new file mode 100644 index 0000000000..354ea7bf70 Binary files /dev/null and b/Resources/Textures/Structures/Specific/anomaly.rsi/anom2-pulse.png differ diff --git a/Resources/Textures/Structures/Specific/anomaly.rsi/anom2.png b/Resources/Textures/Structures/Specific/anomaly.rsi/anom2.png new file mode 100644 index 0000000000..91b21edf71 Binary files /dev/null and b/Resources/Textures/Structures/Specific/anomaly.rsi/anom2.png differ diff --git a/Resources/Textures/Structures/Specific/anomaly.rsi/anom3-pulse.png b/Resources/Textures/Structures/Specific/anomaly.rsi/anom3-pulse.png new file mode 100644 index 0000000000..ab138701ab Binary files /dev/null and b/Resources/Textures/Structures/Specific/anomaly.rsi/anom3-pulse.png differ diff --git a/Resources/Textures/Structures/Specific/anomaly.rsi/anom3.png b/Resources/Textures/Structures/Specific/anomaly.rsi/anom3.png new file mode 100644 index 0000000000..b35962c9de Binary files /dev/null and b/Resources/Textures/Structures/Specific/anomaly.rsi/anom3.png differ diff --git a/Resources/Textures/Structures/Specific/anomaly.rsi/anom4-pulse.png b/Resources/Textures/Structures/Specific/anomaly.rsi/anom4-pulse.png new file mode 100644 index 0000000000..488dd67f1f Binary files /dev/null and b/Resources/Textures/Structures/Specific/anomaly.rsi/anom4-pulse.png differ diff --git a/Resources/Textures/Structures/Specific/anomaly.rsi/anom4.png b/Resources/Textures/Structures/Specific/anomaly.rsi/anom4.png new file mode 100644 index 0000000000..5f362aa4ac Binary files /dev/null and b/Resources/Textures/Structures/Specific/anomaly.rsi/anom4.png differ diff --git a/Resources/Textures/Structures/Specific/anomaly.rsi/meta.json b/Resources/Textures/Structures/Specific/anomaly.rsi/meta.json new file mode 100644 index 0000000000..f9d4be792f --- /dev/null +++ b/Resources/Textures/Structures/Specific/anomaly.rsi/meta.json @@ -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 + ] + ] + } + ] +} \ No newline at end of file diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/anomaly2.png b/Resources/Textures/Structures/Wallmounts/signs.rsi/anomaly2.png new file mode 100644 index 0000000000..73d94ac95e Binary files /dev/null and b/Resources/Textures/Structures/Wallmounts/signs.rsi/anomaly2.png differ diff --git a/Resources/Textures/Structures/Wallmounts/signs.rsi/meta.json b/Resources/Textures/Structures/Wallmounts/signs.rsi/meta.json index 7fb0e4a735..209c9c7d75 100644 --- a/Resources/Textures/Structures/Wallmounts/signs.rsi/meta.json +++ b/Resources/Textures/Structures/Wallmounts/signs.rsi/meta.json @@ -23,6 +23,9 @@ ] ] }, + { + "name": "anomaly2" + }, { "name": "armory", "delays": [