diff --git a/Content.Client/Atmos/Consoles/AtmosAlarmEntryContainer.xaml b/Content.Client/Atmos/Consoles/AtmosAlarmEntryContainer.xaml
new file mode 100644
index 00000000000..96f136abf0e
--- /dev/null
+++ b/Content.Client/Atmos/Consoles/AtmosAlarmEntryContainer.xaml
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Content.Client/Atmos/Consoles/AtmosAlarmEntryContainer.xaml.cs b/Content.Client/Atmos/Consoles/AtmosAlarmEntryContainer.xaml.cs
new file mode 100644
index 00000000000..b0d0365ef6b
--- /dev/null
+++ b/Content.Client/Atmos/Consoles/AtmosAlarmEntryContainer.xaml.cs
@@ -0,0 +1,215 @@
+using Content.Client.Stylesheets;
+using Content.Shared.Atmos;
+using Content.Shared.Atmos.Components;
+using Content.Shared.Atmos.Monitor;
+using Content.Shared.FixedPoint;
+using Content.Shared.Temperature;
+using Robust.Client.AutoGenerated;
+using Robust.Client.Graphics;
+using Robust.Client.ResourceManagement;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Map;
+using System.Linq;
+
+namespace Content.Client.Atmos.Consoles;
+
+[GenerateTypedNameReferences]
+public sealed partial class AtmosAlarmEntryContainer : BoxContainer
+{
+ public NetEntity NetEntity;
+ public EntityCoordinates? Coordinates;
+
+ private readonly IEntityManager _entManager;
+ private readonly IResourceCache _cache;
+
+ private Dictionary _alarmStrings = new Dictionary()
+ {
+ [AtmosAlarmType.Invalid] = "atmos-alerts-window-invalid-state",
+ [AtmosAlarmType.Normal] = "atmos-alerts-window-normal-state",
+ [AtmosAlarmType.Warning] = "atmos-alerts-window-warning-state",
+ [AtmosAlarmType.Danger] = "atmos-alerts-window-danger-state",
+ };
+
+ private Dictionary _gasShorthands = new Dictionary()
+ {
+ [Gas.Ammonia] = "NH₃",
+ [Gas.CarbonDioxide] = "CO₂",
+ [Gas.Frezon] = "F",
+ [Gas.Nitrogen] = "N₂",
+ [Gas.NitrousOxide] = "N₂O",
+ [Gas.Oxygen] = "O₂",
+ [Gas.Plasma] = "P",
+ [Gas.Tritium] = "T",
+ [Gas.WaterVapor] = "H₂O",
+ };
+
+ public AtmosAlarmEntryContainer(NetEntity uid, EntityCoordinates? coordinates)
+ {
+ RobustXamlLoader.Load(this);
+
+ _entManager = IoCManager.Resolve();
+ _cache = IoCManager.Resolve();
+
+ NetEntity = uid;
+ Coordinates = coordinates;
+
+ // Load fonts
+ var headerFont = new VectorFont(_cache.GetResource("/Fonts/NotoSans/NotoSans-Bold.ttf"), 11);
+ var normalFont = new VectorFont(_cache.GetResource("/Fonts/NotoSansDisplay/NotoSansDisplay-Regular.ttf"), 11);
+ var smallFont = new VectorFont(_cache.GetResource("/Fonts/NotoSans/NotoSans-Regular.ttf"), 10);
+
+ // Set fonts
+ TemperatureHeaderLabel.FontOverride = headerFont;
+ PressureHeaderLabel.FontOverride = headerFont;
+ OxygenationHeaderLabel.FontOverride = headerFont;
+ GasesHeaderLabel.FontOverride = headerFont;
+
+ TemperatureLabel.FontOverride = normalFont;
+ PressureLabel.FontOverride = normalFont;
+ OxygenationLabel.FontOverride = normalFont;
+
+ NoDataLabel.FontOverride = headerFont;
+
+ SilenceCheckBox.Label.FontOverride = smallFont;
+ SilenceCheckBox.Label.FontColorOverride = Color.DarkGray;
+ }
+
+ public void UpdateEntry(AtmosAlertsComputerEntry entry, bool isFocus, AtmosAlertsFocusDeviceData? focusData = null)
+ {
+ NetEntity = entry.NetEntity;
+ Coordinates = _entManager.GetCoordinates(entry.Coordinates);
+
+ // Load fonts
+ var normalFont = new VectorFont(_cache.GetResource("/Fonts/NotoSansDisplay/NotoSansDisplay-Regular.ttf"), 11);
+
+ // Update alarm state
+ if (!_alarmStrings.TryGetValue(entry.AlarmState, out var alarmString))
+ alarmString = "atmos-alerts-window-invalid-state";
+
+ AlarmStateLabel.Text = Loc.GetString(alarmString);
+ AlarmStateLabel.FontColorOverride = GetAlarmStateColor(entry.AlarmState);
+
+ // Update alarm name
+ AlarmNameLabel.Text = Loc.GetString("atmos-alerts-window-alarm-label", ("name", entry.EntityName), ("address", entry.Address));
+
+ // Focus updates
+ FocusContainer.Visible = isFocus;
+
+ if (isFocus)
+ SetAsFocus();
+ else
+ RemoveAsFocus();
+
+ if (isFocus && entry.Group == AtmosAlertsComputerGroup.AirAlarm)
+ {
+ MainDataContainer.Visible = (entry.AlarmState != AtmosAlarmType.Invalid);
+ NoDataLabel.Visible = (entry.AlarmState == AtmosAlarmType.Invalid);
+
+ if (focusData != null)
+ {
+ // Update temperature
+ var tempK = (FixedPoint2)focusData.Value.TemperatureData.Item1;
+ var tempC = (FixedPoint2)TemperatureHelpers.KelvinToCelsius(tempK.Float());
+
+ TemperatureLabel.Text = Loc.GetString("atmos-alerts-window-temperature-value", ("valueInC", tempC), ("valueInK", tempK));
+ TemperatureLabel.FontColorOverride = GetAlarmStateColor(focusData.Value.TemperatureData.Item2);
+
+ // Update pressure
+ PressureLabel.Text = Loc.GetString("atmos-alerts-window-pressure-value", ("value", (FixedPoint2)focusData.Value.PressureData.Item1));
+ PressureLabel.FontColorOverride = GetAlarmStateColor(focusData.Value.PressureData.Item2);
+
+ // Update oxygenation
+ var oxygenPercent = (FixedPoint2)0f;
+ var oxygenAlert = AtmosAlarmType.Invalid;
+
+ if (focusData.Value.GasData.TryGetValue(Gas.Oxygen, out var oxygenData))
+ {
+ oxygenPercent = oxygenData.Item2 * 100f;
+ oxygenAlert = oxygenData.Item3;
+ }
+
+ OxygenationLabel.Text = Loc.GetString("atmos-alerts-window-oxygenation-value", ("value", oxygenPercent));
+ OxygenationLabel.FontColorOverride = GetAlarmStateColor(oxygenAlert);
+
+ // Update other present gases
+ GasGridContainer.RemoveAllChildren();
+
+ var gasData = focusData.Value.GasData.Where(g => g.Key != Gas.Oxygen);
+
+ if (gasData.Count() == 0)
+ {
+ // No other gases
+ var gasLabel = new Label()
+ {
+ Text = Loc.GetString("atmos-alerts-window-other-gases-value-nil"),
+ FontOverride = normalFont,
+ FontColorOverride = StyleNano.DisabledFore,
+ HorizontalAlignment = HAlignment.Center,
+ VerticalAlignment = VAlignment.Center,
+ HorizontalExpand = true,
+ Margin = new Thickness(0, 2, 0, 0),
+ SetHeight = 24f,
+ };
+
+ GasGridContainer.AddChild(gasLabel);
+ }
+
+ else
+ {
+ // Add an entry for each gas
+ foreach ((var gas, (var mol, var percent, var alert)) in gasData)
+ {
+ var gasPercent = (FixedPoint2)0f;
+ gasPercent = percent * 100f;
+
+ if (!_gasShorthands.TryGetValue(gas, out var gasShorthand))
+ gasShorthand = "X";
+
+ var gasLabel = new Label()
+ {
+ Text = Loc.GetString("atmos-alerts-window-other-gases-value", ("shorthand", gasShorthand), ("value", gasPercent)),
+ FontOverride = normalFont,
+ FontColorOverride = GetAlarmStateColor(alert),
+ HorizontalAlignment = HAlignment.Center,
+ VerticalAlignment = VAlignment.Center,
+ HorizontalExpand = true,
+ Margin = new Thickness(0, 2, 0, 0),
+ SetHeight = 24f,
+ };
+
+ GasGridContainer.AddChild(gasLabel);
+ }
+ }
+ }
+ }
+ }
+
+ public void SetAsFocus()
+ {
+ FocusButton.AddStyleClass(StyleNano.StyleClassButtonColorGreen);
+ ArrowTexture.TexturePath = "/Textures/Interface/Nano/inverted_triangle.svg.png";
+ }
+
+ public void RemoveAsFocus()
+ {
+ FocusButton.RemoveStyleClass(StyleNano.StyleClassButtonColorGreen);
+ ArrowTexture.TexturePath = "/Textures/Interface/Nano/triangle_right.png";
+ FocusContainer.Visible = false;
+ }
+
+ private Color GetAlarmStateColor(AtmosAlarmType alarmType)
+ {
+ switch (alarmType)
+ {
+ case AtmosAlarmType.Normal:
+ return StyleNano.GoodGreenFore;
+ case AtmosAlarmType.Warning:
+ return StyleNano.ConcerningOrangeFore;
+ case AtmosAlarmType.Danger:
+ return StyleNano.DangerousRedFore;
+ }
+
+ return StyleNano.DisabledFore;
+ }
+}
\ No newline at end of file
diff --git a/Content.Client/Atmos/Consoles/AtmosAlertsComputerBoundUserInterface.cs b/Content.Client/Atmos/Consoles/AtmosAlertsComputerBoundUserInterface.cs
new file mode 100644
index 00000000000..08cae979b9b
--- /dev/null
+++ b/Content.Client/Atmos/Consoles/AtmosAlertsComputerBoundUserInterface.cs
@@ -0,0 +1,52 @@
+using Content.Shared.Atmos.Components;
+
+namespace Content.Client.Atmos.Consoles;
+
+public sealed class AtmosAlertsComputerBoundUserInterface : BoundUserInterface
+{
+ [ViewVariables]
+ private AtmosAlertsComputerWindow? _menu;
+
+ public AtmosAlertsComputerBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) { }
+
+ protected override void Open()
+ {
+ _menu = new AtmosAlertsComputerWindow(this, Owner);
+ _menu.OpenCentered();
+ _menu.OnClose += Close;
+
+ EntMan.TryGetComponent(Owner, out var xform);
+ }
+
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ base.UpdateState(state);
+
+ var castState = (AtmosAlertsComputerBoundInterfaceState) state;
+
+ if (castState == null)
+ return;
+
+ EntMan.TryGetComponent(Owner, out var xform);
+ _menu?.UpdateUI(xform?.Coordinates, castState.AirAlarms, castState.FireAlarms, castState.FocusData);
+ }
+
+ public void SendFocusChangeMessage(NetEntity? netEntity)
+ {
+ SendMessage(new AtmosAlertsComputerFocusChangeMessage(netEntity));
+ }
+
+ public void SendDeviceSilencedMessage(NetEntity netEntity, bool silenceDevice)
+ {
+ SendMessage(new AtmosAlertsComputerDeviceSilencedMessage(netEntity, silenceDevice));
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ if (!disposing)
+ return;
+
+ _menu?.Dispose();
+ }
+}
diff --git a/Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml b/Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml
new file mode 100644
index 00000000000..8824a776ee6
--- /dev/null
+++ b/Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml
@@ -0,0 +1,108 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml.cs b/Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml.cs
new file mode 100644
index 00000000000..a55321833cd
--- /dev/null
+++ b/Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml.cs
@@ -0,0 +1,550 @@
+using Content.Client.Message;
+using Content.Client.Pinpointer.UI;
+using Content.Client.Stylesheets;
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Atmos.Components;
+using Content.Shared.Atmos.Monitor;
+using Content.Shared.Pinpointer;
+using Robust.Client.AutoGenerated;
+using Robust.Client.GameObjects;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Map;
+using Robust.Shared.Timing;
+using Robust.Shared.Utility;
+using Robust.Shared.ContentPack;
+using Robust.Shared.Prototypes;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+
+namespace Content.Client.Atmos.Consoles;
+
+[GenerateTypedNameReferences]
+public sealed partial class AtmosAlertsComputerWindow : FancyWindow
+{
+ private readonly IEntityManager _entManager;
+ private readonly SpriteSystem _spriteSystem;
+
+ private EntityUid? _owner;
+ private NetEntity? _trackedEntity;
+
+ private AtmosAlertsComputerEntry[]? _airAlarms = null;
+ private AtmosAlertsComputerEntry[]? _fireAlarms = null;
+ private IEnumerable? _allAlarms = null;
+
+ private IEnumerable? _activeAlarms = null;
+ private Dictionary _deviceSilencingProgress = new();
+
+ public event Action? SendFocusChangeMessageAction;
+ public event Action? SendDeviceSilencedMessageAction;
+
+ private bool _autoScrollActive = false;
+ private bool _autoScrollAwaitsUpdate = false;
+
+ private const float SilencingDuration = 2.5f;
+
+ public AtmosAlertsComputerWindow(AtmosAlertsComputerBoundUserInterface userInterface, EntityUid? owner)
+ {
+ RobustXamlLoader.Load(this);
+ _entManager = IoCManager.Resolve();
+ _spriteSystem = _entManager.System();
+
+ // Pass the owner to nav map
+ _owner = owner;
+ NavMap.Owner = _owner;
+
+ // Set nav map colors
+ NavMap.WallColor = new Color(64, 64, 64);
+ NavMap.TileColor = Color.DimGray * NavMap.WallColor;
+
+ // Set nav map grid uid
+ var stationName = Loc.GetString("atmos-alerts-window-unknown-location");
+
+ if (_entManager.TryGetComponent(owner, out var xform))
+ {
+ NavMap.MapUid = xform.GridUid;
+
+ // Assign station name
+ if (_entManager.TryGetComponent(xform.GridUid, out var stationMetaData))
+ stationName = stationMetaData.EntityName;
+
+ var msg = new FormattedMessage();
+ msg.AddMarkup(Loc.GetString("atmos-alerts-window-station-name", ("stationName", stationName)));
+
+ StationName.SetMessage(msg);
+ }
+
+ else
+ {
+ StationName.SetMessage(stationName);
+ NavMap.Visible = false;
+ }
+
+ // Set trackable entity selected action
+ NavMap.TrackedEntitySelectedAction += SetTrackedEntityFromNavMap;
+
+ // Update nav map
+ NavMap.ForceNavMapUpdate();
+
+ // Set tab container headers
+ MasterTabContainer.SetTabTitle(0, Loc.GetString("atmos-alerts-window-tab-no-alerts"));
+ MasterTabContainer.SetTabTitle(1, Loc.GetString("atmos-alerts-window-tab-air-alarms"));
+ MasterTabContainer.SetTabTitle(2, Loc.GetString("atmos-alerts-window-tab-fire-alarms"));
+
+ // Set UI toggles
+ ShowInactiveAlarms.OnToggled += _ => OnShowAlarmsToggled(ShowInactiveAlarms, AtmosAlarmType.Invalid);
+ ShowNormalAlarms.OnToggled += _ => OnShowAlarmsToggled(ShowNormalAlarms, AtmosAlarmType.Normal);
+ ShowWarningAlarms.OnToggled += _ => OnShowAlarmsToggled(ShowWarningAlarms, AtmosAlarmType.Warning);
+ ShowDangerAlarms.OnToggled += _ => OnShowAlarmsToggled(ShowDangerAlarms, AtmosAlarmType.Danger);
+
+ // Set atmos monitoring message action
+ SendFocusChangeMessageAction += userInterface.SendFocusChangeMessage;
+ SendDeviceSilencedMessageAction += userInterface.SendDeviceSilencedMessage;
+ }
+
+ #region Toggle handling
+
+ private void OnShowAlarmsToggled(CheckBox toggle, AtmosAlarmType toggledAlarmState)
+ {
+ if (_owner == null)
+ return;
+
+ if (!_entManager.TryGetComponent(_owner.Value, out var console))
+ return;
+
+ foreach (var device in console.AtmosDevices)
+ {
+ var alarmState = GetAlarmState(device.NetEntity);
+
+ if (toggledAlarmState != alarmState)
+ continue;
+
+ if (toggle.Pressed)
+ AddTrackedEntityToNavMap(device, alarmState);
+
+ else
+ NavMap.TrackedEntities.Remove(device.NetEntity);
+ }
+ }
+
+ private void OnSilenceAlertsToggled(NetEntity netEntity, bool toggleState)
+ {
+ if (!_entManager.TryGetComponent(_owner, out var console))
+ return;
+
+ if (toggleState)
+ _deviceSilencingProgress[netEntity] = SilencingDuration;
+
+ else
+ _deviceSilencingProgress.Remove(netEntity);
+
+ foreach (AtmosAlarmEntryContainer entryContainer in AlertsTable.Children)
+ {
+ if (entryContainer.NetEntity == netEntity)
+ entryContainer.SilenceAlarmProgressBar.Visible = toggleState;
+ }
+
+ SendDeviceSilencedMessageAction?.Invoke(netEntity, toggleState);
+ }
+
+ #endregion
+
+ public void UpdateUI(EntityCoordinates? consoleCoords, AtmosAlertsComputerEntry[] airAlarms, AtmosAlertsComputerEntry[] fireAlarms, AtmosAlertsFocusDeviceData? focusData)
+ {
+ if (_owner == null)
+ return;
+
+ if (!_entManager.TryGetComponent(_owner.Value, out var console))
+ return;
+
+ if (_trackedEntity != focusData?.NetEntity)
+ {
+ SendFocusChangeMessageAction?.Invoke(_trackedEntity);
+ focusData = null;
+ }
+
+ // Retain alarm data for use inbetween updates
+ _airAlarms = airAlarms;
+ _fireAlarms = fireAlarms;
+ _allAlarms = airAlarms.Concat(fireAlarms);
+
+ var silenced = console.SilencedDevices;
+
+ _activeAlarms = _allAlarms.Where(x => x.AlarmState > AtmosAlarmType.Normal &&
+ (!silenced.Contains(x.NetEntity) || _deviceSilencingProgress.ContainsKey(x.NetEntity)));
+
+ // Reset nav map data
+ NavMap.TrackedCoordinates.Clear();
+ NavMap.TrackedEntities.Clear();
+
+ // Add tracked entities to the nav map
+ foreach (var device in console.AtmosDevices)
+ {
+ if (!NavMap.Visible)
+ continue;
+
+ var alarmState = GetAlarmState(device.NetEntity);
+
+ if (_trackedEntity != device.NetEntity)
+ {
+ // Skip air alarms if the appropriate overlay is off
+ if (!ShowInactiveAlarms.Pressed && alarmState == AtmosAlarmType.Invalid)
+ continue;
+
+ if (!ShowNormalAlarms.Pressed && alarmState == AtmosAlarmType.Normal)
+ continue;
+
+ if (!ShowWarningAlarms.Pressed && alarmState == AtmosAlarmType.Warning)
+ continue;
+
+ if (!ShowDangerAlarms.Pressed && alarmState == AtmosAlarmType.Danger)
+ continue;
+ }
+
+ AddTrackedEntityToNavMap(device, alarmState);
+ }
+
+ // Show the monitor location
+ var consoleUid = _entManager.GetNetEntity(_owner);
+
+ if (consoleCoords != null && consoleUid != null)
+ {
+ var texture = _spriteSystem.Frame0(new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png")));
+ var blip = new NavMapBlip(consoleCoords.Value, texture, Color.Cyan, true, false);
+ NavMap.TrackedEntities[consoleUid.Value] = blip;
+ }
+
+ // Update the nav map
+ NavMap.ForceNavMapUpdate();
+
+ // Clear excess children from the tables
+ var activeAlarmCount = _activeAlarms.Count();
+
+ while (AlertsTable.ChildCount > activeAlarmCount)
+ AlertsTable.RemoveChild(AlertsTable.GetChild(AlertsTable.ChildCount - 1));
+
+ while (AirAlarmsTable.ChildCount > airAlarms.Length)
+ AirAlarmsTable.RemoveChild(AirAlarmsTable.GetChild(AirAlarmsTable.ChildCount - 1));
+
+ while (FireAlarmsTable.ChildCount > fireAlarms.Length)
+ FireAlarmsTable.RemoveChild(FireAlarmsTable.GetChild(FireAlarmsTable.ChildCount - 1));
+
+ // Update all entries in each table
+ for (int index = 0; index < _activeAlarms.Count(); index++)
+ {
+ var entry = _activeAlarms.ElementAt(index);
+ UpdateUIEntry(entry, index, AlertsTable, console, focusData);
+ }
+
+ for (int index = 0; index < airAlarms.Count(); index++)
+ {
+ var entry = airAlarms.ElementAt(index);
+ UpdateUIEntry(entry, index, AirAlarmsTable, console, focusData);
+ }
+
+ for (int index = 0; index < fireAlarms.Count(); index++)
+ {
+ var entry = fireAlarms.ElementAt(index);
+ UpdateUIEntry(entry, index, FireAlarmsTable, console, focusData);
+ }
+
+ // If no alerts are active, display a message
+ if (MasterTabContainer.CurrentTab == 0 && activeAlarmCount == 0)
+ {
+ var label = new RichTextLabel()
+ {
+ HorizontalExpand = true,
+ VerticalExpand = true,
+ HorizontalAlignment = HAlignment.Center,
+ VerticalAlignment = VAlignment.Center,
+ };
+
+ label.SetMarkup(Loc.GetString("atmos-alerts-window-no-active-alerts", ("color", StyleNano.GoodGreenFore.ToHexNoAlpha())));
+
+ AlertsTable.AddChild(label);
+ }
+
+ // Update the alerts tab with the number of active alerts
+ if (activeAlarmCount == 0)
+ MasterTabContainer.SetTabTitle(0, Loc.GetString("atmos-alerts-window-tab-no-alerts"));
+
+ else
+ MasterTabContainer.SetTabTitle(0, Loc.GetString("atmos-alerts-window-tab-alerts", ("value", activeAlarmCount)));
+
+ // Auto-scroll re-enable
+ if (_autoScrollAwaitsUpdate)
+ {
+ _autoScrollActive = true;
+ _autoScrollAwaitsUpdate = false;
+ }
+ }
+
+ private void AddTrackedEntityToNavMap(AtmosAlertsDeviceNavMapData metaData, AtmosAlarmType alarmState)
+ {
+ var data = GetBlipTexture(alarmState);
+
+ if (data == null)
+ return;
+
+ var texture = data.Value.Item1;
+ var color = data.Value.Item2;
+ var coords = _entManager.GetCoordinates(metaData.NetCoordinates);
+
+ if (_trackedEntity != null && _trackedEntity != metaData.NetEntity)
+ color *= Color.DimGray;
+
+ var selectable = true;
+ var blip = new NavMapBlip(coords, _spriteSystem.Frame0(texture), color, _trackedEntity == metaData.NetEntity, selectable);
+
+ NavMap.TrackedEntities[metaData.NetEntity] = blip;
+ }
+
+ private void UpdateUIEntry(AtmosAlertsComputerEntry entry, int index, Control table, AtmosAlertsComputerComponent console, AtmosAlertsFocusDeviceData? focusData = null)
+ {
+ // Make new UI entry if required
+ if (index >= table.ChildCount)
+ {
+ var newEntryContainer = new AtmosAlarmEntryContainer(entry.NetEntity, _entManager.GetCoordinates(entry.Coordinates));
+
+ // On click
+ newEntryContainer.FocusButton.OnButtonUp += args =>
+ {
+ if (_trackedEntity == newEntryContainer.NetEntity)
+ {
+ _trackedEntity = null;
+ }
+
+ else
+ {
+ _trackedEntity = newEntryContainer.NetEntity;
+
+ if (newEntryContainer.Coordinates != null)
+ NavMap.CenterToCoordinates(newEntryContainer.Coordinates.Value);
+ }
+
+ // Send message to console that the focus has changed
+ SendFocusChangeMessageAction?.Invoke(_trackedEntity);
+
+ // Update affected UI elements across all tables
+ UpdateConsoleTable(console, AlertsTable, _trackedEntity);
+ UpdateConsoleTable(console, AirAlarmsTable, _trackedEntity);
+ UpdateConsoleTable(console, FireAlarmsTable, _trackedEntity);
+ };
+
+ // On toggling the silence check box
+ newEntryContainer.SilenceCheckBox.OnToggled += _ => OnSilenceAlertsToggled(newEntryContainer.NetEntity, newEntryContainer.SilenceCheckBox.Pressed);
+
+ // Add the entry to the current table
+ table.AddChild(newEntryContainer);
+ }
+
+ // Update values and UI elements
+ var tableChild = table.GetChild(index);
+
+ if (tableChild is not AtmosAlarmEntryContainer)
+ {
+ table.RemoveChild(tableChild);
+ UpdateUIEntry(entry, index, table, console, focusData);
+
+ return;
+ }
+
+ var entryContainer = (AtmosAlarmEntryContainer)tableChild;
+
+ entryContainer.UpdateEntry(entry, entry.NetEntity == _trackedEntity, focusData);
+
+ if (_trackedEntity != entry.NetEntity)
+ {
+ var silenced = console.SilencedDevices;
+ entryContainer.SilenceCheckBox.Pressed = (silenced.Contains(entry.NetEntity) || _deviceSilencingProgress.ContainsKey(entry.NetEntity));
+ }
+
+ entryContainer.SilenceAlarmProgressBar.Visible = (table == AlertsTable && _deviceSilencingProgress.ContainsKey(entry.NetEntity));
+ }
+
+ private void UpdateConsoleTable(AtmosAlertsComputerComponent console, Control table, NetEntity? currTrackedEntity)
+ {
+ foreach (var tableChild in table.Children)
+ {
+ if (tableChild is not AtmosAlarmEntryContainer)
+ continue;
+
+ var entryContainer = (AtmosAlarmEntryContainer)tableChild;
+
+ if (entryContainer.NetEntity != currTrackedEntity)
+ entryContainer.RemoveAsFocus();
+
+ else if (entryContainer.NetEntity == currTrackedEntity)
+ entryContainer.SetAsFocus();
+ }
+ }
+
+ private void SetTrackedEntityFromNavMap(NetEntity? netEntity)
+ {
+ if (netEntity == null)
+ return;
+
+ if (!_entManager.TryGetComponent(_owner, out var console))
+ return;
+
+ _trackedEntity = netEntity;
+
+ if (netEntity != null)
+ {
+ // Tab switching
+ if (MasterTabContainer.CurrentTab != 0 || _activeAlarms?.Any(x => x.NetEntity == netEntity) == false)
+ {
+ var device = console.AtmosDevices.FirstOrNull(x => x.NetEntity == netEntity);
+
+ switch (device?.Group)
+ {
+ case AtmosAlertsComputerGroup.AirAlarm:
+ MasterTabContainer.CurrentTab = 1; break;
+ case AtmosAlertsComputerGroup.FireAlarm:
+ MasterTabContainer.CurrentTab = 2; break;
+ }
+ }
+
+ // Get the scroll position of the selected entity on the selected button the UI
+ ActivateAutoScrollToFocus();
+ }
+
+ // Send message to console that the focus has changed
+ SendFocusChangeMessageAction?.Invoke(_trackedEntity);
+ }
+
+ protected override void FrameUpdate(FrameEventArgs args)
+ {
+ AutoScrollToFocus();
+
+ // Device silencing update
+ foreach ((var device, var remainingTime) in _deviceSilencingProgress)
+ {
+ var t = remainingTime - args.DeltaSeconds;
+
+ if (t <= 0)
+ {
+ _deviceSilencingProgress.Remove(device);
+
+ if (device == _trackedEntity)
+ _trackedEntity = null;
+ }
+
+ else
+ _deviceSilencingProgress[device] = t;
+ }
+ }
+
+ private void ActivateAutoScrollToFocus()
+ {
+ _autoScrollActive = false;
+ _autoScrollAwaitsUpdate = true;
+ }
+
+ private void AutoScrollToFocus()
+ {
+ if (!_autoScrollActive)
+ return;
+
+ var scroll = MasterTabContainer.Children.ElementAt(MasterTabContainer.CurrentTab) as ScrollContainer;
+ if (scroll == null)
+ return;
+
+ if (!TryGetVerticalScrollbar(scroll, out var vScrollbar))
+ return;
+
+ if (!TryGetNextScrollPosition(out float? nextScrollPosition))
+ return;
+
+ vScrollbar.ValueTarget = nextScrollPosition.Value;
+
+ if (MathHelper.CloseToPercent(vScrollbar.Value, vScrollbar.ValueTarget))
+ _autoScrollActive = false;
+ }
+
+ private bool TryGetVerticalScrollbar(ScrollContainer scroll, [NotNullWhen(true)] out VScrollBar? vScrollBar)
+ {
+ vScrollBar = null;
+
+ foreach (var child in scroll.Children)
+ {
+ if (child is not VScrollBar)
+ continue;
+
+ var castChild = child as VScrollBar;
+
+ if (castChild != null)
+ {
+ vScrollBar = castChild;
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private bool TryGetNextScrollPosition([NotNullWhen(true)] out float? nextScrollPosition)
+ {
+ nextScrollPosition = null;
+
+ var scroll = MasterTabContainer.Children.ElementAt(MasterTabContainer.CurrentTab) as ScrollContainer;
+ if (scroll == null)
+ return false;
+
+ var container = scroll.Children.ElementAt(0) as BoxContainer;
+ if (container == null || container.Children.Count() == 0)
+ return false;
+
+ // Exit if the heights of the children haven't been initialized yet
+ if (!container.Children.Any(x => x.Height > 0))
+ return false;
+
+ nextScrollPosition = 0;
+
+ foreach (var control in container.Children)
+ {
+ if (control == null || control is not AtmosAlarmEntryContainer)
+ continue;
+
+ if (((AtmosAlarmEntryContainer)control).NetEntity == _trackedEntity)
+ return true;
+
+ nextScrollPosition += control.Height;
+ }
+
+ // Failed to find control
+ nextScrollPosition = null;
+
+ return false;
+ }
+
+ private AtmosAlarmType GetAlarmState(NetEntity netEntity)
+ {
+ var alarmState = _allAlarms?.FirstOrNull(x => x.NetEntity == netEntity)?.AlarmState;
+
+ if (alarmState == null)
+ return AtmosAlarmType.Invalid;
+
+ return alarmState.Value;
+ }
+
+ private (SpriteSpecifier.Texture, Color)? GetBlipTexture(AtmosAlarmType alarmState)
+ {
+ (SpriteSpecifier.Texture, Color)? output = null;
+
+ switch (alarmState)
+ {
+ case AtmosAlarmType.Invalid:
+ output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png")), StyleNano.DisabledFore); break;
+ case AtmosAlarmType.Normal:
+ output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png")), Color.LimeGreen); break;
+ case AtmosAlarmType.Warning:
+ output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_triangle.png")), new Color(255, 182, 72)); break;
+ case AtmosAlarmType.Danger:
+ output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_square.png")), new Color(255, 67, 67)); break;
+ }
+
+ return output;
+ }
+}
\ No newline at end of file
diff --git a/Content.Client/Standing/LayingDownSystem.cs b/Content.Client/Standing/LayingDownSystem.cs
index 594883ac001..3a1f438df05 100644
--- a/Content.Client/Standing/LayingDownSystem.cs
+++ b/Content.Client/Standing/LayingDownSystem.cs
@@ -4,6 +4,7 @@
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Shared.Timing;
+using DrawDepth = Content.Shared.DrawDepth.DrawDepth;
namespace Content.Client.Standing;
@@ -20,6 +21,8 @@ public override void Initialize()
base.Initialize();
SubscribeLocalEvent(OnMovementInput);
+ SubscribeNetworkEvent(OnDowned);
+ SubscribeLocalEvent(OnStood);
SubscribeNetworkEvent(OnCheckAutoGetUp);
}
@@ -48,6 +51,29 @@ private void OnMovementInput(EntityUid uid, LayingDownComponent component, MoveE
sprite.Rotation = Angle.FromDegrees(90);
}
+ private void OnDowned(DrawDownedEvent args)
+ {
+ var uid = GetEntity(args.Uid);
+
+ if (!TryComp(uid, out var sprite)
+ || !TryComp(uid, out var component))
+ return;
+
+ if (!component.OriginalDrawDepth.HasValue)
+ component.OriginalDrawDepth = sprite.DrawDepth;
+
+ sprite.DrawDepth = (int) DrawDepth.SmallMobs;
+ }
+
+ private void OnStood(EntityUid uid, LayingDownComponent component, StoodEvent args)
+ {
+ if (!TryComp(uid, out var sprite)
+ || !component.OriginalDrawDepth.HasValue)
+ return;
+
+ sprite.DrawDepth = component.OriginalDrawDepth.Value;
+ }
+
private void OnCheckAutoGetUp(CheckAutoGetUpEvent ev, EntitySessionEventArgs args)
{
if (!_timing.IsFirstTimePredicted)
diff --git a/Content.Client/Store/Ui/StoreMenu.xaml.cs b/Content.Client/Store/Ui/StoreMenu.xaml.cs
index b7a2c285fe5..7eb597f2f39 100644
--- a/Content.Client/Store/Ui/StoreMenu.xaml.cs
+++ b/Content.Client/Store/Ui/StoreMenu.xaml.cs
@@ -3,6 +3,7 @@
using Content.Client.Message;
using Content.Shared.FixedPoint;
using Content.Shared.Store;
+using Content.Client.Stylesheets;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
@@ -147,6 +148,10 @@ private void AddListingGui(ListingData listing)
}
var newListing = new StoreListingControl(listing, GetListingPriceString(listing), hasBalance, texture);
+
+ if (listing.DiscountValue > 0)
+ newListing.StoreItemBuyButton.AddStyleClass(StyleNano.ButtonColorDangerDefault.ToString());
+
newListing.StoreItemBuyButton.OnButtonDown += args
=> OnListingButtonPressed?.Invoke(args, listing);
diff --git a/Content.IntegrationTests/Tests/Construction/Interaction/MachineConstruction.cs b/Content.IntegrationTests/Tests/Construction/Interaction/MachineConstruction.cs
index f52f820a4ce..cd95a85f205 100644
--- a/Content.IntegrationTests/Tests/Construction/Interaction/MachineConstruction.cs
+++ b/Content.IntegrationTests/Tests/Construction/Interaction/MachineConstruction.cs
@@ -57,4 +57,3 @@ public async Task ChangeMachine()
AssertPrototype("Autolathe");
}
}
-
diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Constants.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Constants.cs
index 11381fb8ccd..a915e5d47d1 100644
--- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Constants.cs
+++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Constants.cs
@@ -1,3 +1,4 @@
+
namespace Content.IntegrationTests.Tests.Interaction;
// This partial class contains various constant prototype IDs common to interaction tests.
@@ -27,8 +28,6 @@ public abstract partial class InteractionTest
// Parts
protected const string Bin1 = "MatterBinStockPart";
- protected const string Cap1 = "CapacitorStockPart";
protected const string Manipulator1 = "MicroManipulatorStockPart";
- protected const string Battery1 = "PowerCellSmall";
- protected const string Battery4 = "PowerCellHyper";
}
+
diff --git a/Content.Server/Abilities/Psionics/Abilities/HealOtherPowerSystem.cs b/Content.Server/Abilities/Psionics/Abilities/HealOtherPowerSystem.cs
new file mode 100644
index 00000000000..85bae78dc6b
--- /dev/null
+++ b/Content.Server/Abilities/Psionics/Abilities/HealOtherPowerSystem.cs
@@ -0,0 +1,152 @@
+using Robust.Shared.Player;
+using Content.Server.DoAfter;
+using Content.Shared.Abilities.Psionics;
+using Content.Shared.Damage;
+using Content.Shared.DoAfter;
+using Content.Shared.Popups;
+using Content.Shared.Psionics.Events;
+using Content.Shared.Examine;
+using static Content.Shared.Examine.ExamineSystemShared;
+using Robust.Shared.Timing;
+using Content.Shared.Actions.Events;
+using Robust.Server.Audio;
+using Content.Server.Atmos.Rotting;
+using Content.Shared.Mobs.Systems;
+using Content.Shared.Mobs;
+using Content.Shared.Mobs.Components;
+using Content.Shared.Psionics.Glimmer;
+
+namespace Content.Server.Abilities.Psionics;
+
+public sealed class RevivifyPowerSystem : EntitySystem
+{
+ [Dependency] private readonly AudioSystem _audioSystem = default!;
+ [Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
+ [Dependency] private readonly SharedPopupSystem _popupSystem = default!;
+ [Dependency] private readonly SharedPsionicAbilitiesSystem _psionics = default!;
+ [Dependency] private readonly IGameTiming _gameTiming = default!;
+ [Dependency] private readonly ExamineSystemShared _examine = default!;
+ [Dependency] private readonly DamageableSystem _damageable = default!;
+ [Dependency] private readonly RottingSystem _rotting = default!;
+ [Dependency] private readonly MobThresholdSystem _mobThreshold = default!;
+ [Dependency] private readonly MobStateSystem _mobState = default!;
+ [Dependency] private readonly GlimmerSystem _glimmer = default!;
+
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnPowerUsed);
+ SubscribeLocalEvent(OnDispelled);
+ SubscribeLocalEvent(OnDoAfter);
+ }
+
+
+ private void OnPowerUsed(EntityUid uid, PsionicComponent component, PsionicHealOtherPowerActionEvent args)
+ {
+ if (component.DoAfter is not null)
+ return;
+
+ if (!args.Immediate)
+ AttemptDoAfter(uid, component, args);
+ else ActivatePower(uid, component, args);
+
+ if (args.PopupText is not null
+ && _glimmer.Glimmer > args.GlimmerObviousPopupThreshold * component.CurrentDampening)
+ _popupSystem.PopupEntity(Loc.GetString(args.PopupText, ("entity", uid)), uid,
+ Filter.Pvs(uid).RemoveWhereAttachedEntity(entity => !_examine.InRangeUnOccluded(uid, entity, ExamineRange, null)),
+ true,
+ args.PopupType);
+
+ if (args.PlaySound
+ && _glimmer.Glimmer > args.GlimmerObviousSoundThreshold * component.CurrentDampening)
+ _audioSystem.PlayPvs(args.SoundUse, uid, args.AudioParams);
+
+ // Sanitize the Glimmer inputs because otherwise the game will crash if someone makes MaxGlimmer lower than MinGlimmer.
+ var minGlimmer = (int) Math.Round(MathF.MinMagnitude(args.MinGlimmer, args.MaxGlimmer)
+ + component.CurrentAmplification - component.CurrentDampening);
+ var maxGlimmer = (int) Math.Round(MathF.MaxMagnitude(args.MinGlimmer, args.MaxGlimmer)
+ + component.CurrentAmplification - component.CurrentDampening);
+
+ _psionics.LogPowerUsed(uid, args.PowerName, minGlimmer, maxGlimmer);
+ args.Handled = true;
+ }
+
+ private void AttemptDoAfter(EntityUid uid, PsionicComponent component, PsionicHealOtherPowerActionEvent args)
+ {
+ var ev = new PsionicHealOtherDoAfterEvent(_gameTiming.CurTime);
+ ev.HealingAmount = args.HealingAmount;
+ ev.RotReduction = args.RotReduction;
+ ev.DoRevive = args.DoRevive;
+ var doAfterArgs = new DoAfterArgs(EntityManager, uid, args.UseDelay, ev, uid, target: args.Target)
+ {
+ BreakOnUserMove = args.BreakOnUserMove,
+ BreakOnTargetMove = args.BreakOnTargetMove,
+ };
+
+ if (!_doAfterSystem.TryStartDoAfter(doAfterArgs, out var doAfterId))
+ return;
+
+ component.DoAfter = doAfterId;
+ }
+
+ private void OnDispelled(EntityUid uid, PsionicComponent component, DispelledEvent args)
+ {
+ if (component.DoAfter is null)
+ return;
+
+ _doAfterSystem.Cancel(component.DoAfter);
+ component.DoAfter = null;
+ args.Handled = true;
+ }
+
+ private void OnDoAfter(EntityUid uid, PsionicComponent component, PsionicHealOtherDoAfterEvent args)
+ {
+ // It's entirely possible for the caster to stop being Psionic(due to mindbreaking) mid cast
+ if (component is null)
+ return;
+ component.DoAfter = null;
+
+ // The target can also cease existing mid-cast
+ if (args.Target is null)
+ return;
+
+ _rotting.ReduceAccumulator(args.Target.Value, TimeSpan.FromSeconds(args.RotReduction * component.CurrentAmplification));
+
+ if (!TryComp(args.Target.Value, out var damageableComponent))
+ return;
+
+ _damageable.TryChangeDamage(args.Target.Value, args.HealingAmount * component.CurrentAmplification, true, false, damageableComponent, uid);
+
+ if (!args.DoRevive
+ || !TryComp(args.Target, out var mob)
+ || !_mobThreshold.TryGetThresholdForState(args.Target.Value, MobState.Dead, out var threshold)
+ || damageableComponent.TotalDamage > threshold)
+ return;
+
+ _mobState.ChangeMobState(args.Target.Value, MobState.Critical, mob, uid);
+ }
+
+ // This would be the same thing as OnDoAfter, except that here the target isn't nullable, so I have to reuse code with different arguments.
+ private void ActivatePower(EntityUid uid, PsionicComponent component, PsionicHealOtherPowerActionEvent args)
+ {
+ if (component is null)
+ return;
+
+ _rotting.ReduceAccumulator(args.Target, TimeSpan.FromSeconds(args.RotReduction * component.CurrentAmplification));
+
+ if (!TryComp(args.Target, out var damageableComponent))
+ return;
+
+ _damageable.TryChangeDamage(args.Target, args.HealingAmount * component.CurrentAmplification, true, false, damageableComponent, uid);
+
+ if (!args.DoRevive
+ || !TryComp(args.Target, out var mob)
+ || !_mobThreshold.TryGetThresholdForState(args.Target, MobState.Dead, out var threshold)
+ || damageableComponent.TotalDamage > threshold)
+ return;
+
+ _mobState.ChangeMobState(args.Target, MobState.Critical, mob, uid);
+ }
+}
diff --git a/Content.Server/Administration/Commands/SetOutfitCommand.cs b/Content.Server/Administration/Commands/SetOutfitCommand.cs
index 12312286518..e19c5b72fa4 100644
--- a/Content.Server/Administration/Commands/SetOutfitCommand.cs
+++ b/Content.Server/Administration/Commands/SetOutfitCommand.cs
@@ -14,6 +14,7 @@
using Robust.Shared.Prototypes;
using Content.Server.Silicon.IPC;
using Content.Shared.Radio.Components;
+using Content.Shared.Cluwne;
namespace Content.Server.Administration.Commands
{
@@ -129,6 +130,8 @@ public static bool SetOutfit(EntityUid target, string gear, IEntityManager entit
}
}
+ if (entityManager.HasComponent(target))
+ return true; //Fuck it, nuclear option for not Cluwning an IPC because that causes a crash that SOMEHOW ignores null checks.
if (entityManager.HasComponent(target))
{
var encryption = new InternalEncryptionKeySpawner();
diff --git a/Content.Server/Anomaly/AnomalySystem.Vessel.cs b/Content.Server/Anomaly/AnomalySystem.Vessel.cs
index 35de5c1a646..9a6c99f8208 100644
--- a/Content.Server/Anomaly/AnomalySystem.Vessel.cs
+++ b/Content.Server/Anomaly/AnomalySystem.Vessel.cs
@@ -1,4 +1,5 @@
using Content.Server.Anomaly.Components;
+using Content.Server.Construction;
using Content.Server.Power.EntitySystems;
using Content.Shared.Anomaly;
using Content.Shared.Anomaly.Components;
@@ -20,6 +21,7 @@ private void InitializeVessel()
{
SubscribeLocalEvent(OnVesselShutdown);
SubscribeLocalEvent(OnVesselMapInit);
+ SubscribeLocalEvent(OnUpgradeExamine);
SubscribeLocalEvent(OnVesselInteractUsing);
SubscribeLocalEvent(OnExamined);
SubscribeLocalEvent(OnVesselGetPointsPerSecond);
@@ -65,6 +67,11 @@ private void OnVesselMapInit(EntityUid uid, AnomalyVesselComponent component, Ma
UpdateVesselAppearance(uid, component);
}
+ private void OnUpgradeExamine(EntityUid uid, AnomalyVesselComponent component, UpgradeExamineEvent args)
+ {
+ args.AddPercentageUpgrade("anomaly-vessel-component-upgrade-output", component.PointMultiplier);
+ }
+
private void OnVesselInteractUsing(EntityUid uid, AnomalyVesselComponent component, InteractUsingEvent args)
{
if (component.Anomaly != null ||
diff --git a/Content.Server/Atmos/Consoles/AtmosAlertsComputerSystem.cs b/Content.Server/Atmos/Consoles/AtmosAlertsComputerSystem.cs
new file mode 100644
index 00000000000..758fde88f13
--- /dev/null
+++ b/Content.Server/Atmos/Consoles/AtmosAlertsComputerSystem.cs
@@ -0,0 +1,356 @@
+using Content.Server.Atmos.Monitor.Components;
+using Content.Server.DeviceNetwork.Components;
+using Content.Server.Power.Components;
+using Content.Shared.Atmos;
+using Content.Shared.Atmos.Components;
+using Content.Shared.Atmos.Consoles;
+using Content.Shared.Atmos.Monitor;
+using Content.Shared.Atmos.Monitor.Components;
+using Content.Shared.Pinpointer;
+using Robust.Server.GameObjects;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Player;
+using Robust.Shared.ContentPack;
+using Robust.Shared.Prototypes;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using Content.Shared.Access.Components;
+using Content.Shared.Database;
+using Content.Shared.NameIdentifier;
+using Content.Shared.Stacks;
+using JetBrains.Annotations;
+using Robust.Shared.Utility;
+
+namespace Content.Server.Atmos.Monitor.Systems;
+
+public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
+{
+ [Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
+ [Dependency] private readonly AirAlarmSystem _airAlarmSystem = default!;
+ [Dependency] private readonly AtmosDeviceNetworkSystem _atmosDevNet = default!;
+ [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
+
+ private const float UpdateTime = 1.0f;
+
+ // Note: this data does not need to be saved
+ private float _updateTimer = 1.0f;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ // Console events
+ SubscribeLocalEvent(OnConsoleInit);
+ SubscribeLocalEvent(OnConsoleParentChanged);
+ SubscribeLocalEvent(OnFocusChangedMessage);
+
+ // Grid events
+ SubscribeLocalEvent(OnGridSplit);
+ SubscribeLocalEvent(OnDeviceAnchorChanged);
+ }
+
+ #region Event handling
+
+ private void OnConsoleInit(EntityUid uid, AtmosAlertsComputerComponent component, ComponentInit args)
+ {
+ InitalizeConsole(uid, component);
+ }
+
+ private void OnConsoleParentChanged(EntityUid uid, AtmosAlertsComputerComponent component, EntParentChangedMessage args)
+ {
+ InitalizeConsole(uid, component);
+ }
+
+ private void OnFocusChangedMessage(EntityUid uid, AtmosAlertsComputerComponent component, AtmosAlertsComputerFocusChangeMessage args)
+ {
+ component.FocusDevice = args.FocusDevice;
+ }
+
+ private void OnGridSplit(ref GridSplitEvent args)
+ {
+ // Collect grids
+ var allGrids = args.NewGrids.ToList();
+
+ if (!allGrids.Contains(args.Grid))
+ allGrids.Add(args.Grid);
+
+ // Update atmos monitoring consoles that stand upon an updated grid
+ var query = AllEntityQuery();
+ while (query.MoveNext(out var ent, out var entConsole, out var entXform))
+ {
+ if (entXform.GridUid == null)
+ continue;
+
+ if (!allGrids.Contains(entXform.GridUid.Value))
+ continue;
+
+ InitalizeConsole(ent, entConsole);
+ }
+ }
+
+ private void OnDeviceAnchorChanged(EntityUid uid, AtmosAlertsDeviceComponent component, AnchorStateChangedEvent args)
+ {
+ var xform = Transform(uid);
+ var gridUid = xform.GridUid;
+
+ if (gridUid == null)
+ return;
+
+ if (!TryGetAtmosDeviceNavMapData(uid, component, xform, gridUid.Value, out var data))
+ return;
+
+ var netEntity = EntityManager.GetNetEntity(uid);
+
+ var query = AllEntityQuery();
+ while (query.MoveNext(out var ent, out var entConsole, out var entXform))
+ {
+ if (gridUid != entXform.GridUid)
+ continue;
+
+ if (args.Anchored)
+ entConsole.AtmosDevices.Add(data.Value);
+
+ else if (!args.Anchored)
+ entConsole.AtmosDevices.RemoveWhere(x => x.NetEntity == netEntity);
+ }
+ }
+
+ #endregion
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ _updateTimer += frameTime;
+
+ if (_updateTimer >= UpdateTime)
+ {
+ _updateTimer -= UpdateTime;
+
+ // Keep a list of UI entries for each gridUid, in case multiple consoles stand on the same grid
+ var airAlarmEntriesForEachGrid = new Dictionary();
+ var fireAlarmEntriesForEachGrid = new Dictionary();
+
+ var query = AllEntityQuery();
+ while (query.MoveNext(out var ent, out var entConsole, out var entXform))
+ {
+ if (entXform?.GridUid == null)
+ continue;
+
+ // Make a list of alarm state data for all the air and fire alarms on the grid
+ if (!airAlarmEntriesForEachGrid.TryGetValue(entXform.GridUid.Value, out var airAlarmEntries))
+ {
+ airAlarmEntries = GetAlarmStateData(entXform.GridUid.Value, AtmosAlertsComputerGroup.AirAlarm).ToArray();
+ airAlarmEntriesForEachGrid[entXform.GridUid.Value] = airAlarmEntries;
+ }
+
+ if (!fireAlarmEntriesForEachGrid.TryGetValue(entXform.GridUid.Value, out var fireAlarmEntries))
+ {
+ fireAlarmEntries = GetAlarmStateData(entXform.GridUid.Value, AtmosAlertsComputerGroup.FireAlarm).ToArray();
+ fireAlarmEntriesForEachGrid[entXform.GridUid.Value] = fireAlarmEntries;
+ }
+
+ // Determine the highest level of alert for the console (based on non-silenced alarms)
+ var highestAlert = AtmosAlarmType.Invalid;
+
+ foreach (var entry in airAlarmEntries)
+ {
+ if (entry.AlarmState > highestAlert && !entConsole.SilencedDevices.Contains(entry.NetEntity))
+ highestAlert = entry.AlarmState;
+ }
+
+ foreach (var entry in fireAlarmEntries)
+ {
+ if (entry.AlarmState > highestAlert && !entConsole.SilencedDevices.Contains(entry.NetEntity))
+ highestAlert = entry.AlarmState;
+ }
+
+ // Update the appearance of the console based on the highest recorded level of alert
+ if (TryComp(ent, out var entAppearance))
+ _appearance.SetData(ent, AtmosAlertsComputerVisuals.ComputerLayerScreen, (int) highestAlert, entAppearance);
+
+ // If the console UI is open, send UI data to each subscribed session
+ UpdateUIState(ent, airAlarmEntries, fireAlarmEntries, entConsole, entXform);
+ }
+ }
+ }
+
+ public void UpdateUIState
+ (EntityUid uid,
+ AtmosAlertsComputerEntry[] airAlarmStateData,
+ AtmosAlertsComputerEntry[] fireAlarmStateData,
+ AtmosAlertsComputerComponent component,
+ TransformComponent xform)
+ {
+ if (!_uiSystem.IsUiOpen(uid, AtmosAlertsComputerUiKey.Key))
+ return;
+
+ var gridUid = xform.GridUid!.Value;
+
+ if (!HasComp(gridUid))
+ return;
+
+ // The grid must have a NavMapComponent to visualize the map in the UI
+ EnsureComp(gridUid);
+
+ // Gathering remaining data to be send to the client
+ var focusAlarmData = GetFocusAlarmData(uid, GetEntity(component.FocusDevice), gridUid);
+
+ // Set the UI state
+ _uiSystem.TrySetUiState(uid, AtmosAlertsComputerUiKey.Key,
+ new AtmosAlertsComputerBoundInterfaceState(airAlarmStateData, fireAlarmStateData, focusAlarmData));
+ }
+
+ private List GetAlarmStateData(EntityUid gridUid, AtmosAlertsComputerGroup group)
+ {
+ var alarmStateData = new List();
+
+ var queryAlarms = AllEntityQuery();
+ while (queryAlarms.MoveNext(out var ent, out var entDevice, out var entAtmosAlarmable, out var entDeviceNetwork, out var entXform))
+ {
+ if (entXform.GridUid != gridUid)
+ continue;
+
+ if (!entXform.Anchored)
+ continue;
+
+ if (entDevice.Group != group)
+ continue;
+
+ // If emagged, change the alarm type to normal
+ var alarmState = (entAtmosAlarmable.LastAlarmState == AtmosAlarmType.Emagged) ? AtmosAlarmType.Normal : entAtmosAlarmable.LastAlarmState;
+
+ // Unpowered alarms can't sound
+ if (TryComp(ent, out var entAPCPower) && !entAPCPower.Powered)
+ alarmState = AtmosAlarmType.Invalid;
+
+ var entry = new AtmosAlertsComputerEntry
+ (GetNetEntity(ent),
+ GetNetCoordinates(entXform.Coordinates),
+ entDevice.Group,
+ alarmState,
+ MetaData(ent).EntityName,
+ entDeviceNetwork.Address);
+
+ alarmStateData.Add(entry);
+ }
+
+ return alarmStateData;
+ }
+
+ private AtmosAlertsFocusDeviceData? GetFocusAlarmData(EntityUid uid, EntityUid? focusDevice, EntityUid gridUid)
+ {
+ if (focusDevice == null)
+ return null;
+
+ var focusDeviceXform = Transform(focusDevice.Value);
+
+ if (!focusDeviceXform.Anchored ||
+ focusDeviceXform.GridUid != gridUid ||
+ !TryComp(focusDevice.Value, out var focusDeviceAirAlarm))
+ {
+ return null;
+ }
+
+ // Force update the sensors attached to the alarm
+ if (!_uiSystem.IsUiOpen(focusDevice.Value, SharedAirAlarmInterfaceKey.Key))
+ {
+ _atmosDevNet.Register(focusDevice.Value, null);
+ _atmosDevNet.Sync(focusDevice.Value, null);
+
+ foreach ((var address, var _) in focusDeviceAirAlarm.SensorData)
+ _atmosDevNet.Register(uid, null);
+ }
+
+ // Get the sensor data
+ var temperatureData = (_airAlarmSystem.CalculateTemperatureAverage(focusDeviceAirAlarm), AtmosAlarmType.Normal);
+ var pressureData = (_airAlarmSystem.CalculatePressureAverage(focusDeviceAirAlarm), AtmosAlarmType.Normal);
+ var gasData = new Dictionary();
+
+ foreach ((var address, var sensorData) in focusDeviceAirAlarm.SensorData)
+ {
+ if (sensorData.TemperatureThreshold.CheckThreshold(sensorData.Temperature, out var temperatureState) &&
+ (int) temperatureState > (int) temperatureData.Item2)
+ {
+ temperatureData = (temperatureData.Item1, temperatureState);
+ }
+
+ if (sensorData.PressureThreshold.CheckThreshold(sensorData.Pressure, out var pressureState) &&
+ (int) pressureState > (int) pressureData.Item2)
+ {
+ pressureData = (pressureData.Item1, pressureState);
+ }
+
+ if (focusDeviceAirAlarm.SensorData.Sum(g => g.Value.TotalMoles) > 1e-8)
+ {
+ foreach ((var gas, var threshold) in sensorData.GasThresholds)
+ {
+ if (!gasData.ContainsKey(gas))
+ {
+ float mol = _airAlarmSystem.CalculateGasMolarConcentrationAverage(focusDeviceAirAlarm, gas, out var percentage);
+
+ if (mol < 1e-8)
+ continue;
+
+ gasData[gas] = (mol, percentage, AtmosAlarmType.Normal);
+ }
+
+ if (threshold.CheckThreshold(gasData[gas].Item2, out var gasState) &&
+ (int) gasState > (int) gasData[gas].Item3)
+ {
+ gasData[gas] = (gasData[gas].Item1, gasData[gas].Item2, gasState);
+ }
+ }
+ }
+ }
+
+ return new AtmosAlertsFocusDeviceData(GetNetEntity(focusDevice.Value), temperatureData, pressureData, gasData);
+ }
+
+ private HashSet GetAllAtmosDeviceNavMapData(EntityUid gridUid)
+ {
+ var atmosDeviceNavMapData = new HashSet();
+
+ var query = AllEntityQuery();
+ while (query.MoveNext(out var ent, out var entComponent, out var entXform))
+ {
+ if (TryGetAtmosDeviceNavMapData(ent, entComponent, entXform, gridUid, out var data))
+ atmosDeviceNavMapData.Add(data.Value);
+ }
+
+ return atmosDeviceNavMapData;
+ }
+
+ private bool TryGetAtmosDeviceNavMapData
+ (EntityUid uid,
+ AtmosAlertsDeviceComponent component,
+ TransformComponent xform,
+ EntityUid gridUid,
+ [NotNullWhen(true)] out AtmosAlertsDeviceNavMapData? output)
+ {
+ output = null;
+
+ if (xform.GridUid != gridUid)
+ return false;
+
+ if (!xform.Anchored)
+ return false;
+
+ output = new AtmosAlertsDeviceNavMapData(GetNetEntity(uid), GetNetCoordinates(xform.Coordinates), component.Group);
+
+ return true;
+ }
+
+ private void InitalizeConsole(EntityUid uid, AtmosAlertsComputerComponent component)
+ {
+ var xform = Transform(uid);
+
+ if (xform.GridUid == null)
+ return;
+
+ var grid = xform.GridUid.Value;
+ component.AtmosDevices = GetAllAtmosDeviceNavMapData(grid);
+
+ Dirty(uid, component);
+ }
+}
diff --git a/Content.Server/Atmos/Monitor/Systems/AirAlarmSystem.cs b/Content.Server/Atmos/Monitor/Systems/AirAlarmSystem.cs
index 2922d0796a9..240f21ad42e 100644
--- a/Content.Server/Atmos/Monitor/Systems/AirAlarmSystem.cs
+++ b/Content.Server/Atmos/Monitor/Systems/AirAlarmSystem.cs
@@ -593,6 +593,21 @@ public float CalculateTemperatureAverage(AirAlarmComponent alarm)
: 0f;
}
+ public float CalculateGasMolarConcentrationAverage(AirAlarmComponent alarm, Gas gas, out float percentage)
+ {
+ percentage = 0f;
+
+ var data = alarm.SensorData.Values.SelectMany(v => v.Gases.Where(g => g.Key == gas));
+
+ if (data.Count() == 0)
+ return 0f;
+
+ var averageMol = data.Select(kvp => kvp.Value).Average();
+ percentage = data.Select(kvp => kvp.Value).Sum() / alarm.SensorData.Values.Select(v => v.TotalMoles).Sum();
+
+ return averageMol;
+ }
+
public void UpdateUI(EntityUid uid, AirAlarmComponent? alarm = null, DeviceNetworkComponent? devNet = null, AtmosAlarmableComponent? alarmable = null)
{
if (!Resolve(uid, ref alarm, ref devNet, ref alarmable))
diff --git a/Content.Server/Atmos/Piping/Binary/Components/GasRecyclerComponent.cs b/Content.Server/Atmos/Piping/Binary/Components/GasRecyclerComponent.cs
index e1eb0072b9d..aa7e3e0b360 100644
--- a/Content.Server/Atmos/Piping/Binary/Components/GasRecyclerComponent.cs
+++ b/Content.Server/Atmos/Piping/Binary/Components/GasRecyclerComponent.cs
@@ -1,11 +1,12 @@
using Content.Shared.Atmos;
+using Content.Shared.Construction.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Atmos.Piping.Binary.Components
{
[RegisterComponent]
public sealed partial class GasRecyclerComponent : Component
{
- [ViewVariables(VVAccess.ReadOnly)]
[DataField("reacting")]
public Boolean Reacting { get; set; } = false;
@@ -17,10 +18,28 @@ public sealed partial class GasRecyclerComponent : Component
[DataField("outlet")]
public string OutletName { get; set; } = "outlet";
- [DataField, ViewVariables(VVAccess.ReadWrite)]
+ [DataField]
public float MinTemp = 300 + Atmospherics.T0C;
- [DataField, ViewVariables(VVAccess.ReadWrite)]
+ [DataField]
+ public float BaseMinTemp = 300 + Atmospherics.T0C;
+
+ [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public string MachinePartMinTemp = "Capacitor";
+
+ [DataField]
+ public float PartRatingMinTempMultiplier = 0.95f;
+
+ [ViewVariables(VVAccess.ReadWrite)]
public float MinPressure = 30 * Atmospherics.OneAtmosphere;
+
+ [DataField]
+ public float BaseMinPressure = 30 * Atmospherics.OneAtmosphere;
+
+ [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public string MachinePartMinPressure = "Manipulator";
+
+ [DataField]
+ public float PartRatingMinPressureMultiplier = 0.8f;
}
}
diff --git a/Content.Server/Atmos/Piping/Binary/EntitySystems/GasRecyclerSystem.cs b/Content.Server/Atmos/Piping/Binary/EntitySystems/GasRecyclerSystem.cs
index 3ebc5094926..40b9d88846f 100644
--- a/Content.Server/Atmos/Piping/Binary/EntitySystems/GasRecyclerSystem.cs
+++ b/Content.Server/Atmos/Piping/Binary/EntitySystems/GasRecyclerSystem.cs
@@ -1,6 +1,7 @@
using Content.Server.Atmos.EntitySystems;
using Content.Server.Atmos.Piping.Binary.Components;
using Content.Server.Atmos.Piping.Components;
+using Content.Server.Construction;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.Nodes;
@@ -28,6 +29,8 @@ public override void Initialize()
SubscribeLocalEvent(OnUpdate);
SubscribeLocalEvent(OnDisabled);
SubscribeLocalEvent(OnExamined);
+ SubscribeLocalEvent(OnRefreshParts);
+ SubscribeLocalEvent(OnUpgradeExamine);
}
private void OnEnabled(EntityUid uid, GasRecyclerComponent comp, ref AtmosDeviceEnabledEvent args)
@@ -116,5 +119,20 @@ private void UpdateAppearance(EntityUid uid, GasRecyclerComponent? comp = null)
_appearance.SetData(uid, PumpVisuals.Enabled, comp.Reacting);
}
+
+ private void OnRefreshParts(EntityUid uid, GasRecyclerComponent component, RefreshPartsEvent args)
+ {
+ var ratingTemp = args.PartRatings[component.MachinePartMinTemp];
+ var ratingPressure = args.PartRatings[component.MachinePartMinPressure];
+
+ component.MinTemp = component.BaseMinTemp * MathF.Pow(component.PartRatingMinTempMultiplier, ratingTemp - 1);
+ component.MinPressure = component.BaseMinPressure * MathF.Pow(component.PartRatingMinPressureMultiplier, ratingPressure - 1);
+ }
+
+ private void OnUpgradeExamine(EntityUid uid, GasRecyclerComponent component, UpgradeExamineEvent args)
+ {
+ args.AddPercentageUpgrade("gas-recycler-upgrade-min-temp", component.MinTemp / component.BaseMinTemp);
+ args.AddPercentageUpgrade("gas-recycler-upgrade-min-pressure", component.MinPressure / component.BaseMinPressure);
+ }
}
}
diff --git a/Content.Server/Atmos/Portable/PortableScrubberComponent.cs b/Content.Server/Atmos/Portable/PortableScrubberComponent.cs
index ae9a5da9639..fbe2d3f95a0 100644
--- a/Content.Server/Atmos/Portable/PortableScrubberComponent.cs
+++ b/Content.Server/Atmos/Portable/PortableScrubberComponent.cs
@@ -1,4 +1,6 @@
using Content.Shared.Atmos;
+using Content.Shared.Construction.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Atmos.Portable
{
@@ -37,13 +39,51 @@ public sealed partial class PortableScrubberComponent : Component
///
/// Maximum internal pressure before it refuses to take more.
///
- [DataField, ViewVariables(VVAccess.ReadWrite)]
+ [DataField]
public float MaxPressure = 2500;
///
- /// The speed at which gas is scrubbed from the environment.
+ /// The base amount of maximum internal pressure
///
- [DataField, ViewVariables(VVAccess.ReadWrite)]
+ [DataField]
+ public float BaseMaxPressure = 2500;
+
+ ///
+ /// The machine part that modifies the maximum internal pressure
+ ///
+ [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public string MachinePartMaxPressure = "MatterBin";
+
+ ///
+ /// How much the will affect the pressure.
+ /// The value will be multiplied by this amount for each increasing part tier.
+ ///
+ [DataField]
+ public float PartRatingMaxPressureModifier = 1.5f;
+
+ ///
+ /// The speed at which gas is scrubbed from the environment.
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
public float TransferRate = 800;
+
+ ///
+ /// The base speed at which gas is scrubbed from the environment.
+ ///
+ [DataField]
+ public float BaseTransferRate = 800;
+
+ ///
+ /// The machine part which modifies the speed of
+ ///
+ [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public string MachinePartTransferRate = "Manipulator";
+
+ ///
+ /// How much the will modify the rate.
+ /// The value will be multiplied by this amount for each increasing part tier.
+ ///
+ [DataField]
+ public float PartRatingTransferRateModifier = 1.4f;
}
}
diff --git a/Content.Server/Atmos/Portable/PortableScrubberSystem.cs b/Content.Server/Atmos/Portable/PortableScrubberSystem.cs
index f9043c091a8..f657d713d28 100644
--- a/Content.Server/Atmos/Portable/PortableScrubberSystem.cs
+++ b/Content.Server/Atmos/Portable/PortableScrubberSystem.cs
@@ -12,6 +12,7 @@
using Content.Server.NodeContainer.NodeGroups;
using Content.Server.Audio;
using Content.Server.Administration.Logs;
+using Content.Server.Construction;
using Content.Server.NodeContainer.EntitySystems;
using Content.Shared.Database;
@@ -38,6 +39,8 @@ public override void Initialize()
SubscribeLocalEvent(OnExamined);
SubscribeLocalEvent(OnDestroyed);
SubscribeLocalEvent(OnScrubberAnalyzed);
+ SubscribeLocalEvent(OnRefreshParts);
+ SubscribeLocalEvent(OnUpgradeExamine);
}
private bool IsFull(PortableScrubberComponent component)
@@ -156,5 +159,20 @@ private void OnScrubberAnalyzed(EntityUid uid, PortableScrubberComponent compone
if (_nodeContainer.TryGetNode(uid, component.PortName, out PipeNode? port))
args.GasMixtures.Add(component.PortName, port.Air);
}
+
+ private void OnRefreshParts(EntityUid uid, PortableScrubberComponent component, RefreshPartsEvent args)
+ {
+ var pressureRating = args.PartRatings[component.MachinePartMaxPressure];
+ var transferRating = args.PartRatings[component.MachinePartTransferRate];
+
+ component.MaxPressure = component.BaseMaxPressure * MathF.Pow(component.PartRatingMaxPressureModifier, pressureRating - 1);
+ component.TransferRate = component.BaseTransferRate * MathF.Pow(component.PartRatingTransferRateModifier, transferRating - 1);
+ }
+
+ private void OnUpgradeExamine(EntityUid uid, PortableScrubberComponent component, UpgradeExamineEvent args)
+ {
+ args.AddPercentageUpgrade("portable-scrubber-component-upgrade-max-pressure", component.MaxPressure / component.BaseMaxPressure);
+ args.AddPercentageUpgrade("portable-scrubber-component-upgrade-transfer-rate", component.TransferRate / component.BaseTransferRate);
+ }
}
}
diff --git a/Content.Server/Bed/BedSystem.cs b/Content.Server/Bed/BedSystem.cs
index f1bd9482e5e..976ef5139c3 100644
--- a/Content.Server/Bed/BedSystem.cs
+++ b/Content.Server/Bed/BedSystem.cs
@@ -2,6 +2,7 @@
using Content.Server.Bed.Components;
using Content.Server.Bed.Sleep;
using Content.Server.Body.Systems;
+using Content.Server.Construction;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Shared.Bed;
@@ -9,6 +10,7 @@
using Content.Shared.Body.Components;
using Content.Shared.Buckle.Components;
using Content.Shared.Damage;
+using Content.Shared.Emag.Components;
using Content.Shared.Emag.Systems;
using Content.Shared.Mobs.Systems;
using Robust.Shared.Timing;
@@ -32,6 +34,8 @@ public override void Initialize()
SubscribeLocalEvent(OnBuckleChange);
SubscribeLocalEvent(OnPowerChanged);
SubscribeLocalEvent(OnEmagged);
+ SubscribeLocalEvent(OnRefreshParts);
+ SubscribeLocalEvent(OnUpgradeExamine);
}
private void ManageUpdateList(EntityUid uid, HealOnBuckleComponent component, ref BuckleChangeEvent args)
@@ -66,7 +70,7 @@ public override void Update(float frameTime)
foreach (var healedEntity in strapComponent.BuckledEntities)
{
- if (_mobStateSystem.IsDead(healedEntity)
+ if (_mobStateSystem.IsDead(healedEntity)
|| HasComp(healedEntity))
continue;
@@ -126,5 +130,18 @@ private void UpdateMetabolisms(EntityUid uid, StasisBedComponent component, bool
RaiseLocalEvent(buckledEntity, ref metabolicEvent);
}
}
+
+ private void OnRefreshParts(EntityUid uid, StasisBedComponent component, RefreshPartsEvent args)
+ {
+ var metabolismRating = args.PartRatings[component.MachinePartMetabolismModifier];
+ component.Multiplier = component.BaseMultiplier * metabolismRating; // Linear scaling so it's not OP
+ if (HasComp(uid))
+ component.Multiplier = 1f / component.Multiplier;
+ }
+
+ private void OnUpgradeExamine(EntityUid uid, StasisBedComponent component, UpgradeExamineEvent args)
+ {
+ args.AddPercentageUpgrade("stasis-bed-component-upgrade-stasis", component.Multiplier / component.BaseMultiplier);
+ }
}
}
diff --git a/Content.Server/Bed/Components/StasisBedComponent.cs b/Content.Server/Bed/Components/StasisBedComponent.cs
index e2175d6e646..bb4096a2a5e 100644
--- a/Content.Server/Bed/Components/StasisBedComponent.cs
+++ b/Content.Server/Bed/Components/StasisBedComponent.cs
@@ -1,12 +1,21 @@
+using Content.Shared.Construction.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
namespace Content.Server.Bed.Components
{
[RegisterComponent]
public sealed partial class StasisBedComponent : Component
{
+ [DataField]
+ public float BaseMultiplier = 10f;
+
///
- /// What the metabolic update rate will be multiplied by (higher = slower metabolism)
+ /// What the metabolic update rate will be multiplied by (higher = slower metabolism)
///
[ViewVariables(VVAccess.ReadWrite)]
public float Multiplier = 10f;
+
+ [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public string MachinePartMetabolismModifier = "Capacitor";
}
}
diff --git a/Content.Server/Botany/Components/SeedExtractorComponent.cs b/Content.Server/Botany/Components/SeedExtractorComponent.cs
index ddb04f213d1..d765e079cec 100644
--- a/Content.Server/Botany/Components/SeedExtractorComponent.cs
+++ b/Content.Server/Botany/Components/SeedExtractorComponent.cs
@@ -1,4 +1,7 @@
using Content.Server.Botany.Systems;
+using Content.Server.Construction;
+using Content.Shared.Construction.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Botany.Components;
@@ -7,14 +10,33 @@ namespace Content.Server.Botany.Components;
public sealed partial class SeedExtractorComponent : Component
{
///
- /// The minimum amount of seed packets dropped.
+ /// The minimum amount of seed packets dropped with no machine upgrades.
///
- [DataField("baseMinSeeds"), ViewVariables(VVAccess.ReadWrite)]
+ [DataField]
public int BaseMinSeeds = 1;
///
- /// The maximum amount of seed packets dropped.
+ /// The maximum amount of seed packets dropped with no machine upgrades.
///
- [DataField("baseMaxSeeds"), ViewVariables(VVAccess.ReadWrite)]
+ [DataField]
public int BaseMaxSeeds = 3;
+
+ ///
+ /// Modifier to the amount of seeds outputted, set on .
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float SeedAmountMultiplier;
+
+ ///
+ /// Machine part whose rating modifies the amount of seed packets dropped.
+ ///
+ [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public string MachinePartSeedAmount = "Manipulator";
+
+ ///
+ /// How much the machine part quality affects the amount of seeds outputted.
+ /// Going up a tier will multiply the seed output by this amount.
+ ///
+ [DataField]
+ public float PartRatingSeedAmountMultiplier = 1.5f;
}
diff --git a/Content.Server/Botany/Systems/SeedExtractorSystem.cs b/Content.Server/Botany/Systems/SeedExtractorSystem.cs
index f1ae6c9f11a..4c547b96f09 100644
--- a/Content.Server/Botany/Systems/SeedExtractorSystem.cs
+++ b/Content.Server/Botany/Systems/SeedExtractorSystem.cs
@@ -1,8 +1,10 @@
using Content.Server.Botany.Components;
+using Content.Server.Construction;
using Content.Server.Popups;
using Content.Server.Power.EntitySystems;
using Content.Shared.Interaction;
using Content.Shared.Popups;
+using Robust.Shared.Player;
using Robust.Shared.Random;
namespace Content.Server.Botany.Systems;
@@ -18,6 +20,8 @@ public override void Initialize()
base.Initialize();
SubscribeLocalEvent(OnInteractUsing);
+ SubscribeLocalEvent(OnRefreshParts);
+ SubscribeLocalEvent(OnUpgradeExamine);
}
private void OnInteractUsing(EntityUid uid, SeedExtractorComponent seedExtractor, InteractUsingEvent args)
@@ -25,8 +29,7 @@ private void OnInteractUsing(EntityUid uid, SeedExtractorComponent seedExtractor
if (!this.IsPowered(uid, EntityManager))
return;
- if (!TryComp(args.Used, out ProduceComponent? produce))
- return;
+ if (!TryComp(args.Used, out ProduceComponent? produce)) return;
if (!_botanySystem.TryGetSeed(produce, out var seed) || seed.Seedless)
{
_popupSystem.PopupCursor(Loc.GetString("seed-extractor-component-no-seeds",("name", args.Used)),
@@ -39,7 +42,7 @@ private void OnInteractUsing(EntityUid uid, SeedExtractorComponent seedExtractor
QueueDel(args.Used);
- var amount = _random.Next(seedExtractor.BaseMinSeeds, seedExtractor.BaseMaxSeeds + 1);
+ var amount = (int) _random.NextFloat(seedExtractor.BaseMinSeeds, seedExtractor.BaseMaxSeeds + 1) * seedExtractor.SeedAmountMultiplier;
var coords = Transform(uid).Coordinates;
if (amount > 1)
@@ -50,4 +53,15 @@ private void OnInteractUsing(EntityUid uid, SeedExtractorComponent seedExtractor
_botanySystem.SpawnSeedPacket(seed, coords, args.User);
}
}
+
+ private void OnRefreshParts(EntityUid uid, SeedExtractorComponent seedExtractor, RefreshPartsEvent args)
+ {
+ var manipulatorQuality = args.PartRatings[seedExtractor.MachinePartSeedAmount];
+ seedExtractor.SeedAmountMultiplier = MathF.Pow(seedExtractor.PartRatingSeedAmountMultiplier, manipulatorQuality - 1);
+ }
+
+ private void OnUpgradeExamine(EntityUid uid, SeedExtractorComponent seedExtractor, UpgradeExamineEvent args)
+ {
+ args.AddPercentageUpgrade("seed-extractor-component-upgrade-seed-yield", seedExtractor.SeedAmountMultiplier);
+ }
}
diff --git a/Content.Server/Cargo/Systems/CargoSystem.Telepad.cs b/Content.Server/Cargo/Systems/CargoSystem.Telepad.cs
index 42aabf2578e..1c25b0e79d2 100644
--- a/Content.Server/Cargo/Systems/CargoSystem.Telepad.cs
+++ b/Content.Server/Cargo/Systems/CargoSystem.Telepad.cs
@@ -1,4 +1,6 @@
using Content.Server.Cargo.Components;
+using Content.Server.Construction;
+using Content.Server.Paper;
using Content.Server.Power.Components;
using Content.Shared.Cargo;
using Content.Shared.Cargo.Components;
@@ -13,6 +15,8 @@ public sealed partial class CargoSystem
private void InitializeTelepad()
{
SubscribeLocalEvent(OnInit);
+ SubscribeLocalEvent(OnRefreshParts);
+ SubscribeLocalEvent(OnUpgradeExamine);
SubscribeLocalEvent(OnTelepadPowerChange);
// Shouldn't need re-anchored event
SubscribeLocalEvent(OnTelepadAnchorChange);
@@ -79,6 +83,17 @@ private void OnInit(EntityUid uid, CargoTelepadComponent telepad, ComponentInit
_linker.EnsureSinkPorts(uid, telepad.ReceiverPort);
}
+ private void OnRefreshParts(EntityUid uid, CargoTelepadComponent component, RefreshPartsEvent args)
+ {
+ var rating = args.PartRatings[component.MachinePartTeleportDelay] - 1;
+ component.Delay = component.BaseDelay * MathF.Pow(component.PartRatingTeleportDelay, rating);
+ }
+
+ private void OnUpgradeExamine(EntityUid uid, CargoTelepadComponent component, UpgradeExamineEvent args)
+ {
+ args.AddPercentageUpgrade("cargo-telepad-delay-upgrade", component.Delay / component.BaseDelay);
+ }
+
private void SetEnabled(EntityUid uid, CargoTelepadComponent component, ApcPowerReceiverComponent? receiver = null,
TransformComponent? xform = null)
{
diff --git a/Content.Server/Chemistry/Components/SolutionHeaterComponent.cs b/Content.Server/Chemistry/Components/SolutionHeaterComponent.cs
index c1841e022c7..bc1d44e82f1 100644
--- a/Content.Server/Chemistry/Components/SolutionHeaterComponent.cs
+++ b/Content.Server/Chemistry/Components/SolutionHeaterComponent.cs
@@ -4,8 +4,26 @@ namespace Content.Server.Chemistry.Components;
public sealed partial class SolutionHeaterComponent : Component
{
///
- /// How much heat is added per second to the solution, taking upgrades into account.
+ /// How much heat is added per second to the solution, with no upgrades.
///
- [DataField, ViewVariables(VVAccess.ReadWrite)]
+ [DataField]
+ public float BaseHeatPerSecond = 120;
+
+ ///
+ /// How much heat is added per second to the solution, taking upgrades into account.
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
public float HeatPerSecond;
+
+ ///
+ /// The machine part that affects the heat multiplier.
+ ///
+ [DataField]
+ public string MachinePartHeatMultiplier = "Capacitor";
+
+ ///
+ /// How much each upgrade multiplies the heat by.
+ ///
+ [DataField]
+ public float PartRatingHeatMultiplier = 1.5f;
}
diff --git a/Content.Server/Chemistry/EntitySystems/SolutionHeaterSystem.cs b/Content.Server/Chemistry/EntitySystems/SolutionHeaterSystem.cs
index 6e6373e10bf..1ef589ab5cb 100644
--- a/Content.Server/Chemistry/EntitySystems/SolutionHeaterSystem.cs
+++ b/Content.Server/Chemistry/EntitySystems/SolutionHeaterSystem.cs
@@ -1,5 +1,6 @@
using Content.Server.Chemistry.Components;
using Content.Server.Chemistry.Containers.EntitySystems;
+using Content.Server.Construction;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Shared.Chemistry;
@@ -20,6 +21,8 @@ public override void Initialize()
base.Initialize();
SubscribeLocalEvent(OnPowerChanged);
+ SubscribeLocalEvent(OnRefreshParts);
+ SubscribeLocalEvent(OnUpgradeExamine);
SubscribeLocalEvent(OnItemPlaced);
SubscribeLocalEvent(OnItemRemoved);
}
@@ -61,6 +64,18 @@ private void OnPowerChanged(Entity entity, ref PowerCha
}
}
+ private void OnRefreshParts(Entity entity, ref RefreshPartsEvent args)
+ {
+ var heatRating = args.PartRatings[entity.Comp.MachinePartHeatMultiplier] - 1;
+
+ entity.Comp.HeatPerSecond = entity.Comp.BaseHeatPerSecond * MathF.Pow(entity.Comp.PartRatingHeatMultiplier, heatRating);
+ }
+
+ private void OnUpgradeExamine(Entity entity, ref UpgradeExamineEvent args)
+ {
+ args.AddPercentageUpgrade("solution-heater-upgrade-heat", entity.Comp.HeatPerSecond / entity.Comp.BaseHeatPerSecond);
+ }
+
private void OnItemPlaced(Entity entity, ref ItemPlacedEvent args)
{
TryTurnOn(entity);
diff --git a/Content.Server/Chemistry/ReagentEffects/MakeSentient.cs b/Content.Server/Chemistry/ReagentEffects/MakeSentient.cs
index 5654f9067b5..a84e21b997e 100644
--- a/Content.Server/Chemistry/ReagentEffects/MakeSentient.cs
+++ b/Content.Server/Chemistry/ReagentEffects/MakeSentient.cs
@@ -41,7 +41,7 @@ public override void Effect(ReagentEffectArgs args)
if (!knowledge.SpokenLanguages.Contains(fallback))
knowledge.SpokenLanguages.Add(fallback);
- IoCManager.Resolve().GetEntitySystem().UpdateEntityLanguages(uid, speaker);
+ IoCManager.Resolve().GetEntitySystem().UpdateEntityLanguages(uid);
// Stops from adding a ghost role to things like people who already have a mind
if (entityManager.TryGetComponent(uid, out var mindContainer) && mindContainer.HasMind)
diff --git a/Content.Server/Cloning/CloningSystem.cs b/Content.Server/Cloning/CloningSystem.cs
index 7931fae4778..72104bc381f 100644
--- a/Content.Server/Cloning/CloningSystem.cs
+++ b/Content.Server/Cloning/CloningSystem.cs
@@ -1,6 +1,7 @@
using Content.Server.Atmos.EntitySystems;
using Content.Server.Chat.Systems;
using Content.Server.Cloning.Components;
+using Content.Server.Construction;
using Content.Server.DeviceLinking.Systems;
using Content.Server.EUI;
using Content.Server.Fluids.EntitySystems;
@@ -9,6 +10,10 @@
using Content.Server.Materials;
using Content.Server.Popups;
using Content.Server.Power.EntitySystems;
+using Content.Server.Traits.Assorted;
+using Content.Shared.Atmos;
+using Content.Shared.CCVar;
+using Content.Shared.Chemistry.Components;
using Content.Shared.Cloning;
using Content.Shared.Damage;
using Content.Shared.DeviceLinking.Events;
@@ -90,8 +95,23 @@ public override void Initialize()
SubscribeLocalEvent(OnExamined);
SubscribeLocalEvent(OnEmagged);
SubscribeLocalEvent(OnPowerChanged);
+ SubscribeLocalEvent(OnPartsRefreshed);
+ SubscribeLocalEvent(OnUpgradeExamine);
+ }
+ private void OnPartsRefreshed(EntityUid uid, CloningPodComponent component, RefreshPartsEvent args)
+ {
+ var materialRating = args.PartRatings[component.MachinePartMaterialUse];
+ var speedRating = args.PartRatings[component.MachinePartCloningSpeed];
+
+ component.BiomassCostMultiplier = MathF.Pow(component.PartRatingMaterialMultiplier, materialRating - 1);
+ component.CloningTime = component.CloningTime * MathF.Pow(component.PartRatingSpeedMultiplier, speedRating - 1);
}
+ private void OnUpgradeExamine(EntityUid uid, CloningPodComponent component, UpgradeExamineEvent args)
+ {
+ args.AddPercentageUpgrade("cloning-pod-component-upgrade-speed", component.CloningTime / component.CloningTime);
+ args.AddPercentageUpgrade("cloning-pod-component-upgrade-biomass-requirement", component.BiomassCostMultiplier);
+ }
private void OnPortDisconnected(EntityUid uid, CloningPodComponent pod, PortDisconnectedEvent args)
{
pod.ConnectedConsole = null;
diff --git a/Content.Server/Construction/ConstructionSystem.Machine.cs b/Content.Server/Construction/ConstructionSystem.Machine.cs
index 2e670dbe40d..65b0b704761 100644
--- a/Content.Server/Construction/ConstructionSystem.Machine.cs
+++ b/Content.Server/Construction/ConstructionSystem.Machine.cs
@@ -5,6 +5,7 @@
using Content.Shared.Construction.Prototypes;
using Content.Shared.Verbs;
using Robust.Shared.Containers;
+using Robust.Shared.Map.Components;
using Robust.Shared.Utility;
namespace Content.Server.Construction;
@@ -143,7 +144,7 @@ private void CreateBoardAndStockParts(EntityUid uid, MachineComponent component)
var p = EntityManager.SpawnEntity(partProto.StockPartPrototype, xform.Coordinates);
if (!_container.Insert(p, partContainer))
- throw new Exception($"Couldn't insert machine part of type {part} to machine with prototype {partProto.StockPartPrototype}!");
+ throw new Exception($"Couldn't insert machine part of type {part} to machine with prototype {partProto.StockPartPrototype ?? "N/A"}!");
}
}
@@ -183,7 +184,7 @@ public sealed class RefreshPartsEvent : EntityEventArgs
{
public IReadOnlyList Parts = new List();
- public Dictionary PartRatings = new();
+ public Dictionary PartRatings = new Dictionary();
}
public sealed class UpgradeExamineEvent : EntityEventArgs
diff --git a/Content.Server/Construction/PartExchangerSystem.cs b/Content.Server/Construction/PartExchangerSystem.cs
index ee5edcbd0a0..f364d1b547d 100644
--- a/Content.Server/Construction/PartExchangerSystem.cs
+++ b/Content.Server/Construction/PartExchangerSystem.cs
@@ -10,6 +10,7 @@
using Robust.Shared.Containers;
using Robust.Shared.Utility;
using Content.Shared.Wires;
+using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Collections;
@@ -42,7 +43,7 @@ private void OnDoAfter(EntityUid uid, PartExchangerComponent component, DoAfterE
if (args.Handled || args.Args.Target == null)
return;
- if (!TryComp(uid, out var storage))
+ if (!TryComp(uid, out var storage) || storage.Container == null)
return; //the parts are stored in here
var machinePartQuery = GetEntityQuery();
diff --git a/Content.Server/Flight/FlightSystem.cs b/Content.Server/Flight/FlightSystem.cs
index e056fc24ec0..39321b1e66c 100644
--- a/Content.Server/Flight/FlightSystem.cs
+++ b/Content.Server/Flight/FlightSystem.cs
@@ -6,6 +6,7 @@
using Content.Shared.Flight.Events;
using Content.Shared.Mobs;
using Content.Shared.Popups;
+using Content.Shared.Standing;
using Content.Shared.Stunnable;
using Content.Shared.Zombies;
using Robust.Shared.Audio.Systems;
@@ -16,6 +17,7 @@ public sealed class FlightSystem : SharedFlightSystem
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
+ [Dependency] private readonly StandingStateSystem _standing = default!;
public override void Initialize()
{
@@ -27,6 +29,7 @@ public override void Initialize()
SubscribeLocalEvent(OnZombified);
SubscribeLocalEvent(OnKnockedDown);
SubscribeLocalEvent(OnStunned);
+ SubscribeLocalEvent(OnDowned);
SubscribeLocalEvent(OnSleep);
}
public override void Update(float frameTime)
@@ -103,6 +106,13 @@ private bool CanFly(EntityUid uid, FlightComponent component)
_popupSystem.PopupEntity(Loc.GetString("no-flight-while-zombified"), uid, uid, PopupType.Medium);
return false;
}
+
+ if (HasComp(uid) && _standing.IsDown(uid))
+ {
+ _popupSystem.PopupEntity(Loc.GetString("no-flight-while-lying"), uid, uid, PopupType.Medium);
+ return false;
+ }
+
return true;
}
@@ -142,6 +152,14 @@ private void OnStunned(EntityUid uid, FlightComponent component, ref StunnedEven
ToggleActive(uid, false, component);
}
+ private void OnDowned(EntityUid uid, FlightComponent component, ref DownedEvent args)
+ {
+ if (!component.On)
+ return;
+
+ ToggleActive(uid, false, component);
+ }
+
private void OnSleep(EntityUid uid, FlightComponent component, ref SleepStateChangedEvent args)
{
if (!component.On
diff --git a/Content.Server/Gravity/GravityGeneratorComponent.cs b/Content.Server/Gravity/GravityGeneratorComponent.cs
index f9462920384..f47d3979391 100644
--- a/Content.Server/Gravity/GravityGeneratorComponent.cs
+++ b/Content.Server/Gravity/GravityGeneratorComponent.cs
@@ -37,6 +37,9 @@ public sealed partial class GravityGeneratorComponent : SharedGravityGeneratorCo
// 0 -> 1
[ViewVariables(VVAccess.ReadWrite)] [DataField("charge")] public float Charge { get; set; } = 1;
+ [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public string MachinePartMaxChargeMultiplier = "Capacitor";
+
///
/// Is the gravity generator currently "producing" gravity?
///
diff --git a/Content.Server/Gravity/GravityGeneratorSystem.cs b/Content.Server/Gravity/GravityGeneratorSystem.cs
index ec5646457e2..b1696e6a713 100644
--- a/Content.Server/Gravity/GravityGeneratorSystem.cs
+++ b/Content.Server/Gravity/GravityGeneratorSystem.cs
@@ -1,5 +1,6 @@
using Content.Server.Administration.Logs;
using Content.Server.Audio;
+using Content.Server.Construction;
using Content.Server.Power.Components;
using Content.Server.Emp;
using Content.Shared.Database;
@@ -27,6 +28,7 @@ public override void Initialize()
SubscribeLocalEvent(OnComponentShutdown);
SubscribeLocalEvent(OnParentChanged); // Or just anchor changed?
SubscribeLocalEvent(OnInteractHand);
+ SubscribeLocalEvent(OnRefreshParts);
SubscribeLocalEvent(
OnSwitchGenerator);
@@ -257,6 +259,12 @@ public void UpdateState(Entity ent, AppearanceComponent? appearance)
{
_ambientSoundSystem.SetAmbience(ent, false);
diff --git a/Content.Server/Kitchen/Components/MicrowaveComponent.cs b/Content.Server/Kitchen/Components/MicrowaveComponent.cs
index 815ba8f5213..1e343e5e332 100644
--- a/Content.Server/Kitchen/Components/MicrowaveComponent.cs
+++ b/Content.Server/Kitchen/Components/MicrowaveComponent.cs
@@ -11,95 +11,99 @@ namespace Content.Server.Kitchen.Components
[RegisterComponent]
public sealed partial class MicrowaveComponent : Component
{
- [DataField("cookTimeMultiplier"), ViewVariables(VVAccess.ReadWrite)]
+ [DataField]
public float CookTimeMultiplier = 1;
-
- [DataField("baseHeatMultiplier"), ViewVariables(VVAccess.ReadWrite)]
+ [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public string MachinePartCookTimeMultiplier = "Capacitor";
+ [DataField]
+ public float CookTimeScalingConstant = 0.5f;
+ [DataField]
public float BaseHeatMultiplier = 100;
- [DataField("objectHeatMultiplier"), ViewVariables(VVAccess.ReadWrite)]
+ [DataField]
public float ObjectHeatMultiplier = 100;
- [DataField("failureResult", customTypeSerializer: typeof(PrototypeIdSerializer))]
+ [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))]
public string BadRecipeEntityId = "FoodBadRecipe";
#region audio
- [DataField("beginCookingSound")]
+ [DataField]
public SoundSpecifier StartCookingSound = new SoundPathSpecifier("/Audio/Machines/microwave_start_beep.ogg");
- [DataField("foodDoneSound")]
+ [DataField]
public SoundSpecifier FoodDoneSound = new SoundPathSpecifier("/Audio/Machines/microwave_done_beep.ogg");
- [DataField("clickSound")]
+ [DataField]
public SoundSpecifier ClickSound = new SoundPathSpecifier("/Audio/Machines/machine_switch.ogg");
- [DataField("ItemBreakSound")]
+ [DataField]
public SoundSpecifier ItemBreakSound = new SoundPathSpecifier("/Audio/Effects/clang.ogg");
public EntityUid? PlayingStream;
- [DataField("loopingSound")]
+ [DataField]
public SoundSpecifier LoopingSound = new SoundPathSpecifier("/Audio/Machines/microwave_loop.ogg");
#endregion
[ViewVariables]
public bool Broken;
- [DataField, ViewVariables(VVAccess.ReadWrite)]
+ [DataField]
public ProtoId OnPort = "On";
///
- /// This is a fixed offset of 5.
- /// The cook times for all recipes should be divisible by 5,with a minimum of 1 second.
- /// For right now, I don't think any recipe cook time should be greater than 60 seconds.
+ /// This is a fixed offset of 5.
+ /// The cook times for all recipes should be divisible by 5,with a minimum of 1 second.
+ /// For right now, I don't think any recipe cook time should be greater than 60 seconds.
///
- [DataField("currentCookTimerTime"), ViewVariables(VVAccess.ReadWrite)]
+ [DataField]
public uint CurrentCookTimerTime = 0;
///
- /// Tracks the elapsed time of the current cook timer.
+ /// Tracks the elapsed time of the current cook timer.
///
- [DataField, ViewVariables(VVAccess.ReadWrite)]
+ [DataField]
public TimeSpan CurrentCookTimeEnd = TimeSpan.Zero;
///
- /// The maximum number of seconds a microwave can be set to.
- /// This is currently only used for validation and the client does not check this.
+ /// The maximum number of seconds a microwave can be set to.
+ /// This is currently only used for validation and the client does not check this.
///
- [DataField("maxCookTime"), ViewVariables(VVAccess.ReadWrite)]
+ [DataField]
public uint MaxCookTime = 30;
///
/// The max temperature that this microwave can heat objects to.
///
- [DataField("temperatureUpperThreshold")]
+ [DataField]
public float TemperatureUpperThreshold = 373.15f;
public int CurrentCookTimeButtonIndex;
public Container Storage = default!;
- [DataField, ViewVariables(VVAccess.ReadWrite)]
+ [DataField]
public int Capacity = 10;
- [DataField, ViewVariables(VVAccess.ReadWrite)]
+ [DataField]
public ProtoId MaxItemSize = "Normal";
///
- /// How frequently the microwave can malfunction.
+ /// How frequently the microwave can malfunction.
///
[DataField]
public float MalfunctionInterval = 1.0f;
///
- /// Chance of an explosion occurring when we microwave a metallic object
+ /// Chance of an explosion occurring when we microwave a metallic object
///
- [DataField, ViewVariables(VVAccess.ReadWrite)]
+ [DataField]
public float ExplosionChance = .1f;
///
- /// Chance of lightning occurring when we microwave a metallic object
- [DataField, ViewVariables(VVAccess.ReadWrite)]
+ /// Chance of lightning occurring when we microwave a metallic object
+ ///
+ [DataField]
public float LightningChance = .75f;
}
diff --git a/Content.Server/Kitchen/Components/ReagentGrinderComponent.cs b/Content.Server/Kitchen/Components/ReagentGrinderComponent.cs
index 5bbbe2dc8da..4f4531206c7 100644
--- a/Content.Server/Kitchen/Components/ReagentGrinderComponent.cs
+++ b/Content.Server/Kitchen/Components/ReagentGrinderComponent.cs
@@ -1,6 +1,8 @@
using Content.Shared.Kitchen;
using Content.Server.Kitchen.EntitySystems;
+using Content.Shared.Construction.Prototypes;
using Robust.Shared.Audio;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Kitchen.Components
{
@@ -13,15 +15,30 @@ namespace Content.Server.Kitchen.Components
[Access(typeof(ReagentGrinderSystem)), RegisterComponent]
public sealed partial class ReagentGrinderComponent : Component
{
- [DataField]
+ [ViewVariables(VVAccess.ReadWrite)]
public int StorageMaxEntities = 6;
[DataField]
- public TimeSpan WorkTime = TimeSpan.FromSeconds(3.5); // Roughly matches the grind/juice sounds.
+ public int BaseStorageMaxEntities = 4;
+
+ [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public string MachinePartStorageMax = "MatterBin";
+
+ [DataField]
+ public int StoragePerPartRating = 4;
[DataField]
+ public TimeSpan WorkTime = TimeSpan.FromSeconds(3.5); // Roughly matches the grind/juice sounds.
+
+ [ViewVariables(VVAccess.ReadWrite)]
public float WorkTimeMultiplier = 1;
+ [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public string MachinePartWorkTime = "Manipulator";
+
+ [DataField]
+ public float PartRatingWorkTimerMulitplier = 0.6f;
+
[DataField]
public SoundSpecifier ClickSound { get; set; } = new SoundPathSpecifier("/Audio/Machines/machine_switch.ogg");
diff --git a/Content.Server/Kitchen/EntitySystems/MicrowaveSystem.cs b/Content.Server/Kitchen/EntitySystems/MicrowaveSystem.cs
index 212383c463a..3de7051f54d 100644
--- a/Content.Server/Kitchen/EntitySystems/MicrowaveSystem.cs
+++ b/Content.Server/Kitchen/EntitySystems/MicrowaveSystem.cs
@@ -76,6 +76,8 @@ public override void Initialize()
SubscribeLocalEvent(OnPowerChanged);
SubscribeLocalEvent(OnAnchorChanged);
SubscribeLocalEvent(OnSuicide);
+ SubscribeLocalEvent(OnRefreshParts);
+ SubscribeLocalEvent(OnUpgradeExamine);
SubscribeLocalEvent(OnSignalReceived);
@@ -342,6 +344,17 @@ private void OnAnchorChanged(EntityUid uid, MicrowaveComponent component, ref An
_container.EmptyContainer(component.Storage);
}
+ private void OnRefreshParts(Entity ent, ref RefreshPartsEvent args)
+ {
+ var cookRating = args.PartRatings[ent.Comp.MachinePartCookTimeMultiplier];
+ ent.Comp.CookTimeMultiplier = MathF.Pow(ent.Comp.CookTimeScalingConstant, cookRating - 1);
+ }
+
+ private void OnUpgradeExamine(Entity ent, ref UpgradeExamineEvent args)
+ {
+ args.AddPercentageUpgrade("microwave-component-upgrade-cook-time", ent.Comp.CookTimeMultiplier);
+ }
+
private void OnSignalReceived(Entity ent, ref SignalReceivedEvent args)
{
if (args.Port != ent.Comp.OnPort)
diff --git a/Content.Server/Kitchen/EntitySystems/ReagentGrinderSystem.cs b/Content.Server/Kitchen/EntitySystems/ReagentGrinderSystem.cs
index e8ee4539860..aad33fea678 100644
--- a/Content.Server/Kitchen/EntitySystems/ReagentGrinderSystem.cs
+++ b/Content.Server/Kitchen/EntitySystems/ReagentGrinderSystem.cs
@@ -1,4 +1,5 @@
using Content.Server.Chemistry.Containers.EntitySystems;
+using Content.Server.Construction;
using Content.Server.Kitchen.Components;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
@@ -48,6 +49,8 @@ public override void Initialize()
SubscribeLocalEvent((uid, _, _) => UpdateUiState(uid));
SubscribeLocalEvent((EntityUid uid, ReagentGrinderComponent _, ref PowerChangedEvent _) => UpdateUiState(uid));
SubscribeLocalEvent(OnInteractUsing);
+ SubscribeLocalEvent(OnRefreshParts);
+ SubscribeLocalEvent(OnUpgradeExamine);
SubscribeLocalEvent(OnContainerModified);
SubscribeLocalEvent(OnContainerModified);
@@ -197,6 +200,24 @@ private void OnInteractUsing(Entity entity, ref Interac
args.Handled = true;
}
+ ///
+ /// Gotta be efficient, you know? you're saving a whole extra second here and everything.
+ ///
+ private void OnRefreshParts(Entity entity, ref RefreshPartsEvent args)
+ {
+ var ratingWorkTime = args.PartRatings[entity.Comp.MachinePartWorkTime];
+ var ratingStorage = args.PartRatings[entity.Comp.MachinePartStorageMax];
+
+ entity.Comp.WorkTimeMultiplier = MathF.Pow(entity.Comp.PartRatingWorkTimerMulitplier, ratingWorkTime - 1);
+ entity.Comp.StorageMaxEntities = entity.Comp.BaseStorageMaxEntities + (int) (entity.Comp.StoragePerPartRating * (ratingStorage - 1));
+ }
+
+ private void OnUpgradeExamine(Entity entity, ref UpgradeExamineEvent args)
+ {
+ args.AddPercentageUpgrade("reagent-grinder-component-upgrade-work-time", entity.Comp.WorkTimeMultiplier);
+ args.AddNumberUpgrade("reagent-grinder-component-upgrade-storage", entity.Comp.StorageMaxEntities - entity.Comp.BaseStorageMaxEntities);
+ }
+
private void UpdateUiState(EntityUid uid)
{
ReagentGrinderComponent? grinderComp = null;
diff --git a/Content.Server/Language/LanguageSystem.Networking.cs b/Content.Server/Language/LanguageSystem.Networking.cs
index 572e2961fde..5f7f2742734 100644
--- a/Content.Server/Language/LanguageSystem.Networking.cs
+++ b/Content.Server/Language/LanguageSystem.Networking.cs
@@ -64,12 +64,7 @@ private void SendLanguageStateToClient(ICommonSession session, LanguageSpeakerCo
// TODO this is really stupid and can be avoided if we just make everything shared...
private void SendLanguageStateToClient(EntityUid uid, ICommonSession session, LanguageSpeakerComponent? component = null)
{
- var isUniversal = HasComp(uid);
- if (!isUniversal)
- Resolve(uid, ref component, logMissing: false);
-
- // I really don't want to call 3 getter methods here, so we'll just have this slightly hardcoded solution
- var message = isUniversal || component == null
+ var message = !Resolve(uid, ref component, logMissing: false)
? new LanguagesUpdatedMessage(UniversalPrototype, [UniversalPrototype], [UniversalPrototype])
: new LanguagesUpdatedMessage(component.CurrentLanguage, component.SpokenLanguages, component.UnderstoodLanguages);
diff --git a/Content.Server/Language/LanguageSystem.cs b/Content.Server/Language/LanguageSystem.cs
index e68489e9e28..a1c30997e2d 100644
--- a/Content.Server/Language/LanguageSystem.cs
+++ b/Content.Server/Language/LanguageSystem.cs
@@ -2,7 +2,6 @@
using Content.Server.Language.Events;
using Content.Shared.Language;
using Content.Shared.Language.Components;
-using Content.Shared.Language.Events;
using Content.Shared.Language.Systems;
using UniversalLanguageSpeakerComponent = Content.Shared.Language.Components.UniversalLanguageSpeakerComponent;
@@ -16,8 +15,19 @@ public override void Initialize()
InitializeNet();
SubscribeLocalEvent(OnInitLanguageSpeaker);
+ SubscribeLocalEvent(OnUniversalInit);
+ SubscribeLocalEvent(OnUniversalShutdown);
}
+ private void OnUniversalShutdown(EntityUid uid, UniversalLanguageSpeakerComponent component, ComponentShutdown args)
+ {
+ RemoveLanguage(uid, UniversalPrototype);
+ }
+
+ private void OnUniversalInit(EntityUid uid, UniversalLanguageSpeakerComponent component, MapInitEvent args)
+ {
+ AddLanguage(uid, UniversalPrototype);
+ }
#region public api
@@ -48,10 +58,9 @@ public bool CanSpeak(EntityUid speaker, string language, LanguageSpeakerComponen
///
public LanguagePrototype GetLanguage(EntityUid speaker, LanguageSpeakerComponent? component = null)
{
- if (HasComp(speaker) || !Resolve(speaker, ref component, logMissing: false))
- return Universal; // Serves both as a fallback and uhhh something (TODO: fix this comment)
-
- if (string.IsNullOrEmpty(component.CurrentLanguage) || !_prototype.TryIndex(component.CurrentLanguage, out var proto))
+ if (!Resolve(speaker, ref component, logMissing: false)
+ || string.IsNullOrEmpty(component.CurrentLanguage)
+ || !_prototype.TryIndex(component.CurrentLanguage, out var proto))
return Universal;
return proto;
@@ -63,13 +72,10 @@ public LanguagePrototype GetLanguage(EntityUid speaker, LanguageSpeakerComponent
/// Typically, checking is sufficient.
public List GetSpokenLanguages(EntityUid uid)
{
- if (HasComp(uid))
- return [UniversalPrototype];
+ if (!TryComp(uid, out var component))
+ return [];
- if (TryComp(uid, out var component))
- return component.SpokenLanguages;
-
- return [];
+ return component.SpokenLanguages;
}
///
@@ -78,21 +84,17 @@ public List GetSpokenLanguages(EntityUid uid)
/// Typically, checking is sufficient.
public List GetUnderstoodLanguages(EntityUid uid)
{
- if (HasComp(uid))
- return [UniversalPrototype]; // This one is tricky because... well, they understand all of them, not just one.
-
- if (TryComp(uid, out var component))
- return component.UnderstoodLanguages;
+ if (!TryComp(uid, out var component))
+ return [];
- return [];
+ return component.UnderstoodLanguages;
}
public void SetLanguage(EntityUid speaker, string language, LanguageSpeakerComponent? component = null)
{
- if (!CanSpeak(speaker, language) || (HasComp(speaker) && language != UniversalPrototype))
- return;
-
- if (!Resolve(speaker, ref component) || component.CurrentLanguage == language)
+ if (!CanSpeak(speaker, language)
+ || !Resolve(speaker, ref component)
+ || component.CurrentLanguage == language)
return;
component.CurrentLanguage = language;
@@ -106,12 +108,10 @@ public void AddLanguage(
EntityUid uid,
string language,
bool addSpoken = true,
- bool addUnderstood = true,
- LanguageKnowledgeComponent? knowledge = null,
- LanguageSpeakerComponent? speaker = null)
+ bool addUnderstood = true)
{
- if (knowledge == null)
- knowledge = EnsureComp(uid);
+ EnsureComp(uid, out var knowledge);
+ EnsureComp(uid);
if (addSpoken && !knowledge.SpokenLanguages.Contains(language))
knowledge.SpokenLanguages.Add(language);
@@ -119,7 +119,7 @@ public void AddLanguage(
if (addUnderstood && !knowledge.UnderstoodLanguages.Contains(language))
knowledge.UnderstoodLanguages.Add(language);
- UpdateEntityLanguages(uid, speaker);
+ UpdateEntityLanguages(uid);
}
///
@@ -129,12 +129,10 @@ public void RemoveLanguage(
EntityUid uid,
string language,
bool removeSpoken = true,
- bool removeUnderstood = true,
- LanguageKnowledgeComponent? knowledge = null,
- LanguageSpeakerComponent? speaker = null)
+ bool removeUnderstood = true)
{
- if (knowledge == null)
- knowledge = EnsureComp(uid);
+ if (!TryComp(uid, out var knowledge))
+ return;
if (removeSpoken)
knowledge.SpokenLanguages.Remove(language);
@@ -142,7 +140,7 @@ public void RemoveLanguage(
if (removeUnderstood)
knowledge.UnderstoodLanguages.Remove(language);
- UpdateEntityLanguages(uid, speaker);
+ UpdateEntityLanguages(uid);
}
///
@@ -168,9 +166,9 @@ public bool EnsureValidLanguage(EntityUid entity, LanguageSpeakerComponent? comp
///
/// Immediately refreshes the cached lists of spoken and understood languages for the given entity.
///
- public void UpdateEntityLanguages(EntityUid entity, LanguageSpeakerComponent? languages = null)
+ public void UpdateEntityLanguages(EntityUid entity)
{
- if (!Resolve(entity, ref languages))
+ if (!TryComp(entity, out var languages))
return;
var ev = new DetermineEntityLanguagesEvent();
@@ -205,7 +203,7 @@ private void OnInitLanguageSpeaker(EntityUid uid, LanguageSpeakerComponent compo
if (string.IsNullOrEmpty(component.CurrentLanguage))
component.CurrentLanguage = component.SpokenLanguages.FirstOrDefault(UniversalPrototype);
- UpdateEntityLanguages(uid, component);
+ UpdateEntityLanguages(uid);
}
#endregion
diff --git a/Content.Server/Language/TranslatorSystem.cs b/Content.Server/Language/TranslatorSystem.cs
index a893993e884..c48b93a3930 100644
--- a/Content.Server/Language/TranslatorSystem.cs
+++ b/Content.Server/Language/TranslatorSystem.cs
@@ -85,8 +85,8 @@ private void OnTranslatorParentChanged(EntityUid translator, HandheldTranslatorC
// If that is not the case, then OnProxyDetermineLanguages will remove this translator from HoldsTranslatorComponent.Translators.
Timer.Spawn(0, () =>
{
- if (Exists(args.OldParent) && TryComp(args.OldParent, out var speaker))
- _language.UpdateEntityLanguages(args.OldParent.Value, speaker);
+ if (Exists(args.OldParent) && HasComp(args.OldParent))
+ _language.UpdateEntityLanguages(args.OldParent.Value);
});
}
@@ -108,7 +108,7 @@ private void OnTranslatorToggle(EntityUid translator, HandheldTranslatorComponen
{
// The first new spoken language added by this translator, or null
var firstNewLanguage = translatorComp.SpokenLanguages.FirstOrDefault(it => !languageComp.SpokenLanguages.Contains(it));
- _language.UpdateEntityLanguages(holder, languageComp);
+ _language.UpdateEntityLanguages(holder);
// Update the current language of the entity if necessary
if (isEnabled && translatorComp.SetLanguageOnInteract && firstNewLanguage is {})
@@ -131,8 +131,8 @@ private void OnPowerCellSlotEmpty(EntityUid translator, HandheldTranslatorCompon
_powerCell.SetPowerCellDrawEnabled(translator, false);
OnAppearanceChange(translator, component);
- if (_containers.TryGetContainingContainer(translator, out var holderCont) && TryComp(holderCont.Owner, out var languageComp))
- _language.UpdateEntityLanguages(holderCont.Owner, languageComp);
+ if (_containers.TryGetContainingContainer(translator, out var holderCont) && HasComp(holderCont.Owner))
+ _language.UpdateEntityLanguages(holderCont.Owner);
}
private void CopyLanguages(BaseTranslatorComponent from, DetermineEntityLanguagesEvent to, LanguageKnowledgeComponent knowledge)
diff --git a/Content.Server/Materials/MaterialReclaimerSystem.cs b/Content.Server/Materials/MaterialReclaimerSystem.cs
index aa24fde44b7..de82f125985 100644
--- a/Content.Server/Materials/MaterialReclaimerSystem.cs
+++ b/Content.Server/Materials/MaterialReclaimerSystem.cs
@@ -1,4 +1,6 @@
using Content.Server.Chemistry.Containers.EntitySystems;
+using Content.Server.Chemistry.EntitySystems;
+using Content.Server.Construction;
using Content.Server.Fluids.EntitySystems;
using Content.Server.GameTicking;
using Content.Server.Popups;
@@ -45,6 +47,8 @@ public override void Initialize()
base.Initialize();
SubscribeLocalEvent(OnStartup);
+ SubscribeLocalEvent(OnRefreshParts);
+ SubscribeLocalEvent(OnUpgradeExamine);
SubscribeLocalEvent(OnPowerChanged);
SubscribeLocalEvent(OnInteractUsing,
before: new []{typeof(WiresSystem), typeof(SolutionTransferSystem)});
@@ -56,6 +60,18 @@ private void OnStartup(Entity entity, ref ComponentS
_solutionContainer.EnsureSolution(entity.Owner, entity.Comp.SolutionContainerId);
}
+ private void OnUpgradeExamine(Entity entity, ref UpgradeExamineEvent args)
+ {
+ args.AddPercentageUpgrade(Loc.GetString("material-reclaimer-upgrade-process-rate"), entity.Comp.MaterialProcessRate / entity.Comp.BaseMaterialProcessRate);
+ }
+
+ private void OnRefreshParts(Entity entity, ref RefreshPartsEvent args)
+ {
+ var rating = args.PartRatings[entity.Comp.MachinePartProcessRate] - 1;
+ entity.Comp.MaterialProcessRate = entity.Comp.BaseMaterialProcessRate * MathF.Pow(entity.Comp.PartRatingProcessRateMultiplier, rating);
+ Dirty(entity);
+ }
+
private void OnPowerChanged(Entity entity, ref PowerChangedEvent args)
{
AmbientSound.SetAmbience(entity.Owner, entity.Comp.Enabled && args.Powered);
diff --git a/Content.Server/Medical/BiomassReclaimer/BiomassReclaimerComponent.cs b/Content.Server/Medical/BiomassReclaimer/BiomassReclaimerComponent.cs
index 61d36f98b96..1358bfbcbbc 100644
--- a/Content.Server/Medical/BiomassReclaimer/BiomassReclaimerComponent.cs
+++ b/Content.Server/Medical/BiomassReclaimer/BiomassReclaimerComponent.cs
@@ -1,4 +1,8 @@
+using System.Threading;
+using Content.Shared.Construction.Prototypes;
using Content.Shared.Storage;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Medical.BiomassReclaimer
{
@@ -6,72 +10,111 @@ namespace Content.Server.Medical.BiomassReclaimer
public sealed partial class BiomassReclaimerComponent : Component
{
///
- /// This gets set for each mob it processes.
- /// When it hits 0, there is a chance for the reclaimer to either spill blood or throw an item.
+ /// This gets set for each mob it processes.
+ /// When it hits 0, there is a chance for the reclaimer to either spill blood or throw an item.
///
[ViewVariables]
public float RandomMessTimer = 0f;
///
- /// The interval for .
+ /// The interval for .
///
- [ViewVariables(VVAccess.ReadWrite), DataField]
+ [DataField]
public TimeSpan RandomMessInterval = TimeSpan.FromSeconds(5);
///
- /// This gets set for each mob it processes.
- /// When it hits 0, spit out biomass.
+ /// This gets set for each mob it processes.
+ /// When it hits 0, spit out biomass.
///
[ViewVariables]
- public float ProcessingTimer = default;
+ public float ProcessingTimer;
///
- /// Amount of biomass that the mob being processed will yield.
- /// This is calculated from the YieldPerUnitMass.
- /// Also stores non-integer leftovers.
+ /// Amount of biomass that the mob being processed will yield.
+ /// This is calculated from the YieldPerUnitMass.
+ /// Also stores non-integer leftovers.
///
[ViewVariables]
- public float CurrentExpectedYield = 0f;
+ public float CurrentExpectedYield;
///
- /// The reagent that will be spilled while processing a mob.
+ /// The reagent that will be spilled while processing a mob.
///
[ViewVariables]
public string? BloodReagent;
///
- /// Entities that can be randomly spawned while processing a mob.
+ /// Entities that can be randomly spawned while processing a mob.
///
public List SpawnedEntities = new();
///
- /// How many units of biomass it produces for each unit of mass.
+ /// How many units of biomass it produces for each unit of mass.
///
- [DataField, ViewVariables(VVAccess.ReadWrite)]
- public float YieldPerUnitMass = 0.4f;
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float YieldPerUnitMass = default;
///
- /// How many seconds to take to insert an entity per unit of its mass.
+ /// The base yield per mass unit when no components are upgraded.
///
- [DataField, ViewVariables(VVAccess.ReadWrite)]
+ [DataField]
+ public float BaseYieldPerUnitMass = 0.4f;
+
+ ///
+ /// Machine part whose rating modifies the yield per mass.
+ ///
+ [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public string MachinePartYieldAmount = "MatterBin";
+
+ ///
+ /// How much the machine part quality affects the yield.
+ /// Going up a tier will multiply the yield by this amount.
+ ///
+ [DataField]
+ public float PartRatingYieldAmountMultiplier = 1.25f;
+
+ ///
+ /// How many seconds to take to insert an entity per unit of its mass.
+ ///
+ [DataField]
public float BaseInsertionDelay = 0.1f;
///
- /// How much to multiply biomass yield from botany produce.
+ /// How much to multiply biomass yield from botany produce.
///
- [DataField, ViewVariables(VVAccess.ReadWrite)]
+ [DataField]
public float ProduceYieldMultiplier = 0.25f;
///
- /// The time it takes to process a mob, per mass.
+ /// The time it takes to process a mob, per mass.
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float ProcessingTimePerUnitMass;
+
+ ///
+ /// The base time per mass unit that it takes to process a mob
+ /// when no components are upgraded.
+ ///
+ [DataField]
+ public float BaseProcessingTimePerUnitMass = 0.5f;
+
+ ///
+ /// The machine part that increses the processing speed.
+ ///
+ [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public string MachinePartProcessingSpeed = "Manipulator";
+
+ ///
+ /// How much the machine part quality affects the yield.
+ /// Going up a tier will multiply the speed by this amount.
///
- [DataField, ViewVariables(VVAccess.ReadWrite)]
- public float ProcessingTimePerUnitMass = 0.5f;
+ [DataField]
+ public float PartRatingSpeedMultiplier = 1.35f;
///
- /// Will this refuse to gib a living mob?
+ /// Will this refuse to gib a living mob?
///
- [ViewVariables(VVAccess.ReadWrite), DataField]
+ [DataField]
public bool SafetyEnabled = true;
}
}
diff --git a/Content.Server/Medical/BiomassReclaimer/BiomassReclaimerSystem.cs b/Content.Server/Medical/BiomassReclaimer/BiomassReclaimerSystem.cs
index eaf04d64b2b..97a758a5ed3 100644
--- a/Content.Server/Medical/BiomassReclaimer/BiomassReclaimerSystem.cs
+++ b/Content.Server/Medical/BiomassReclaimer/BiomassReclaimerSystem.cs
@@ -1,6 +1,7 @@
using System.Numerics;
using Content.Server.Body.Components;
using Content.Server.Botany.Components;
+using Content.Server.Construction;
using Content.Server.Fluids.EntitySystems;
using Content.Server.Materials;
using Content.Server.Power.Components;
@@ -99,6 +100,8 @@ public override void Initialize()
SubscribeLocalEvent(OnUnanchorAttempt);
SubscribeLocalEvent(OnAfterInteractUsing);
SubscribeLocalEvent(OnClimbedOn);
+ SubscribeLocalEvent(OnRefreshParts);
+ SubscribeLocalEvent(OnUpgradeExamine);
SubscribeLocalEvent(OnPowerChanged);
SubscribeLocalEvent(OnSuicide);
SubscribeLocalEvent(OnDoAfter);
@@ -173,6 +176,26 @@ private void OnClimbedOn(Entity reclaimer, ref Climbe
StartProcessing(args.Climber, reclaimer);
}
+ private void OnRefreshParts(EntityUid uid, BiomassReclaimerComponent component, RefreshPartsEvent args)
+ {
+ var laserRating = args.PartRatings[component.MachinePartProcessingSpeed];
+ var manipRating = args.PartRatings[component.MachinePartYieldAmount];
+
+ // Processing time slopes downwards with part rating.
+ component.ProcessingTimePerUnitMass =
+ component.BaseProcessingTimePerUnitMass / MathF.Pow(component.PartRatingSpeedMultiplier, laserRating - 1);
+
+ // Yield slopes upwards with part rating.
+ component.YieldPerUnitMass =
+ component.BaseYieldPerUnitMass * MathF.Pow(component.PartRatingYieldAmountMultiplier, manipRating - 1);
+ }
+
+ private void OnUpgradeExamine(EntityUid uid, BiomassReclaimerComponent component, UpgradeExamineEvent args)
+ {
+ args.AddPercentageUpgrade("biomass-reclaimer-component-upgrade-speed", component.BaseProcessingTimePerUnitMass / component.ProcessingTimePerUnitMass);
+ args.AddPercentageUpgrade("biomass-reclaimer-component-upgrade-biomass-yield", component.YieldPerUnitMass / component.BaseYieldPerUnitMass);
+ }
+
private void OnDoAfter(Entity reclaimer, ref ReclaimerDoAfterEvent args)
{
if (args.Handled
diff --git a/Content.Server/Medical/Components/MedicalScannerComponent.cs b/Content.Server/Medical/Components/MedicalScannerComponent.cs
index 96de6499875..15ca6cd2bd7 100644
--- a/Content.Server/Medical/Components/MedicalScannerComponent.cs
+++ b/Content.Server/Medical/Components/MedicalScannerComponent.cs
@@ -1,5 +1,4 @@
using Content.Shared.Construction.Prototypes;
-using Content.Shared.DragDrop;
using Content.Shared.MedicalScanner;
using Robust.Shared.Containers;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
@@ -13,10 +12,15 @@ public sealed partial class MedicalScannerComponent : SharedMedicalScannerCompon
public ContainerSlot BodyContainer = default!;
public EntityUid? ConnectedConsole;
- [DataField, ViewVariables(VVAccess.ReadWrite)]
+ [ViewVariables(VVAccess.ReadWrite)]
public float CloningFailChanceMultiplier = 1f;
-
- // Nyano, needed for Metem Machine.
+
public float MetemKarmaBonus = 0.25f;
+
+ [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public string MachinePartCloningFailChance = "Capacitor";
+
+ [DataField]
+ public float PartRatingFailMultiplier = 0.75f;
}
}
diff --git a/Content.Server/Medical/MedicalScannerSystem.cs b/Content.Server/Medical/MedicalScannerSystem.cs
index a6ce43c4081..ab6918e373b 100644
--- a/Content.Server/Medical/MedicalScannerSystem.cs
+++ b/Content.Server/Medical/MedicalScannerSystem.cs
@@ -7,6 +7,7 @@
using Content.Shared.Verbs;
using Robust.Shared.Containers;
using Content.Server.Cloning.Components;
+using Content.Server.Construction;
using Content.Server.DeviceLinking.Systems;
using Content.Shared.DeviceLinking.Events;
using Content.Server.Power.EntitySystems;
@@ -44,6 +45,8 @@ public override void Initialize()
SubscribeLocalEvent(OnDragDropOn);
SubscribeLocalEvent(OnPortDisconnected);
SubscribeLocalEvent(OnAnchorChanged);
+ SubscribeLocalEvent(OnRefreshParts);
+ SubscribeLocalEvent(OnUpgradeExamine);
SubscribeLocalEvent(OnCanDragDropOn);
}
@@ -220,5 +223,17 @@ public void EjectBody(EntityUid uid, MedicalScannerComponent? scannerComponent)
_climbSystem.ForciblySetClimbing(contained, uid);
UpdateAppearance(uid, scannerComponent);
}
+
+ private void OnRefreshParts(EntityUid uid, MedicalScannerComponent component, RefreshPartsEvent args)
+ {
+ var ratingFail = args.PartRatings[component.MachinePartCloningFailChance];
+
+ component.CloningFailChanceMultiplier = MathF.Pow(component.PartRatingFailMultiplier, ratingFail - 1);
+ }
+
+ private void OnUpgradeExamine(EntityUid uid, MedicalScannerComponent component, UpgradeExamineEvent args)
+ {
+ args.AddPercentageUpgrade("medical-scanner-upgrade-cloning", component.CloningFailChanceMultiplier);
+ }
}
}
diff --git a/Content.Server/NPC/Components/NPCJukeComponent.cs b/Content.Server/NPC/Components/NPCJukeComponent.cs
index 2c4136c24b9..768feeca6fc 100644
--- a/Content.Server/NPC/Components/NPCJukeComponent.cs
+++ b/Content.Server/NPC/Components/NPCJukeComponent.cs
@@ -1,4 +1,3 @@
-using Content.Server.NPC.HTN.PrimitiveTasks.Operators.Combat;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.NPC.Components;
@@ -6,17 +5,20 @@ namespace Content.Server.NPC.Components;
[RegisterComponent, AutoGenerateComponentPause]
public sealed partial class NPCJukeComponent : Component
{
- [DataField("jukeType")]
+ [DataField]
public JukeType JukeType = JukeType.Away;
- [DataField("jukeDuration")]
+ [DataField]
public float JukeDuration = 0.5f;
- [DataField("nextJuke", customTypeSerializer:typeof(TimeOffsetSerializer))]
+ [DataField]
+ public float JukeCooldown = 3f;
+
+ [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
[AutoPausedField]
public TimeSpan NextJuke;
- [DataField("targetTile")]
+ [DataField]
public Vector2i? TargetTile;
}
diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/JukeOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/JukeOperator.cs
index 02a3b085104..68029f5a4c2 100644
--- a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/JukeOperator.cs
+++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/JukeOperator.cs
@@ -6,17 +6,31 @@ public sealed partial class JukeOperator : HTNOperator, IHtnConditionalShutdown
{
[Dependency] private readonly IEntityManager _entManager = default!;
- [DataField("jukeType")]
+ [DataField]
public JukeType JukeType = JukeType.AdjacentTile;
- [DataField("shutdownState")]
+ [DataField]
public HTNPlanState ShutdownState { get; private set; } = HTNPlanState.PlanFinished;
+ ///
+ /// Controls how long(in seconds) the NPC will move while juking.
+ ///
+ [DataField]
+ public float JukeDuration = 0.5f;
+
+ ///
+ /// Controls how often (in seconds) an NPC will try to juke.
+ ///
+ [DataField]
+ public float JukeCooldown = 3f;
+
public override void Startup(NPCBlackboard blackboard)
{
base.Startup(blackboard);
var juke = _entManager.EnsureComponent(blackboard.GetValue(NPCBlackboard.Owner));
juke.JukeType = JukeType;
+ juke.JukeDuration = JukeDuration;
+ juke.JukeCooldown = JukeCooldown;
}
public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime)
diff --git a/Content.Server/NPC/Systems/NPCJukeSystem.cs b/Content.Server/NPC/Systems/NPCJukeSystem.cs
index da9fa1f7615..5a724762ef6 100644
--- a/Content.Server/NPC/Systems/NPCJukeSystem.cs
+++ b/Content.Server/NPC/Systems/NPCJukeSystem.cs
@@ -3,6 +3,7 @@
using Content.Server.NPC.Events;
using Content.Server.NPC.HTN.PrimitiveTasks.Operators.Combat;
using Content.Server.Weapons.Melee;
+using Content.Shared.Coordinates.Helpers;
using Content.Shared.NPC;
using Content.Shared.Weapons.Melee;
using Robust.Shared.Collections;
@@ -38,22 +39,19 @@ public override void Initialize()
private void OnJukeSteering(EntityUid uid, NPCJukeComponent component, ref NPCSteeringEvent args)
{
- if (component.JukeType == JukeType.AdjacentTile)
+ if (_timing.CurTime < component.NextJuke)
{
- if (_npcRangedQuery.TryGetComponent(uid, out var ranged) &&
- ranged.Status == CombatStatus.NotInSight)
- {
- component.TargetTile = null;
- return;
- }
+ component.TargetTile = null;
+ return;
+ }
- if (_timing.CurTime < component.NextJuke)
- {
- component.TargetTile = null;
- return;
- }
+ component.NextJuke = _timing.CurTime + TimeSpan.FromSeconds(component.JukeCooldown);
- if (!TryComp(args.Transform.GridUid, out var grid))
+ if (component.JukeType == JukeType.AdjacentTile)
+ {
+ if (_npcRangedQuery.TryGetComponent(uid, out var ranged)
+ && ranged.Status is CombatStatus.NotInSight
+ || !TryComp(args.Transform.GridUid, out var grid))
{
component.TargetTile = null;
return;
@@ -107,12 +105,11 @@ private void OnJukeSteering(EntityUid uid, NPCJukeComponent component, ref NPCSt
var elapsed = _timing.CurTime - component.NextJuke;
- // Finished juke, reset timer.
- if (elapsed.TotalSeconds > component.JukeDuration ||
- currentTile == component.TargetTile)
+ // Finished juke.
+ if (elapsed.TotalSeconds > component.JukeDuration
+ || currentTile == component.TargetTile)
{
component.TargetTile = null;
- component.NextJuke = _timing.CurTime + TimeSpan.FromSeconds(component.JukeDuration);
return;
}
@@ -155,9 +152,7 @@ private void OnJukeSteering(EntityUid uid, NPCJukeComponent component, ref NPCSt
var obstacleDirection = _transform.GetWorldPosition(melee.Target) - args.WorldPosition;
if (obstacleDirection == Vector2.Zero)
- {
obstacleDirection = _random.NextVector2();
- }
// If they're moving away then pursue anyway.
// If just hit then always back up a bit.
diff --git a/Content.Server/Nutrition/Components/FatExtractorComponent.cs b/Content.Server/Nutrition/Components/FatExtractorComponent.cs
index e23c557236c..fa6edc911e1 100644
--- a/Content.Server/Nutrition/Components/FatExtractorComponent.cs
+++ b/Content.Server/Nutrition/Components/FatExtractorComponent.cs
@@ -1,4 +1,5 @@
using Content.Server.Nutrition.EntitySystems;
+using Content.Shared.Construction.Prototypes;
using Content.Shared.Nutrition.Components;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
@@ -8,67 +9,87 @@
namespace Content.Server.Nutrition.Components;
///
-/// This is used for a machine that extracts hunger from entities and creates meat. Yum!
+/// This is used for a machine that extracts hunger from entities and creates meat. Yum!
///
[RegisterComponent, Access(typeof(FatExtractorSystem)), AutoGenerateComponentPause]
public sealed partial class FatExtractorComponent : Component
{
///
- /// Whether or not the extractor is currently extracting fat from someone
+ /// Whether or not the extractor is currently extracting fat from someone
///
- [DataField("processing")]
+ [DataField]
public bool Processing = true;
///
- /// How much nutrition is extracted per second.
+ /// How much nutrition is extracted per second.
///
- [DataField("nutritionPerSecond"), ViewVariables(VVAccess.ReadWrite)]
+ [ViewVariables(VVAccess.ReadWrite)]
public int NutritionPerSecond = 10;
///
- /// An accumulator which tracks extracted nutrition to determine
- /// when to spawn a meat.
+ /// The base rate of extraction
///
- [DataField("nutrientAccumulator"), ViewVariables(VVAccess.ReadWrite)]
+ [DataField]
+ public int BaseNutritionPerSecond = 10;
+
+ #region Machine Upgrade
+ ///
+ /// Which machine part affects the nutrition rate
+ ///
+ [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public string MachinePartNutritionRate = "Manipulator";
+
+ ///
+ /// The increase in rate per each rating above 1.
+ ///
+ [DataField]
+ public float PartRatingRateMultiplier = 10;
+ #endregion
+
+ ///
+ /// An accumulator which tracks extracted nutrition to determine
+ /// when to spawn a meat.
+ ///
+ [DataField]
public int NutrientAccumulator;
///
- /// How high has to be to spawn meat
+ /// How high has to be to spawn meat
///
- [DataField("nutrientPerMeat"), ViewVariables(VVAccess.ReadWrite)]
- public int NutrientPerMeat = 30;
+ [DataField]
+ public int NutrientPerMeat = 60;
///
- /// Meat spawned by the extractor.
+ /// Meat spawned by the extractor.
///
- [DataField("meatPrototype", customTypeSerializer: typeof(PrototypeIdSerializer)), ViewVariables(VVAccess.ReadWrite)]
+ [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))]
public string MeatPrototype = "FoodMeat";
///
- /// When the next update will occur
+ /// When the next update will occur
///
- [DataField("nextUpdate", customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)]
+ [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
[AutoPausedField]
public TimeSpan NextUpdate;
///
- /// How long each update takes
+ /// How long each update takes
///
- [DataField("updateTime"), ViewVariables(VVAccess.ReadWrite)]
+ [DataField]
public TimeSpan UpdateTime = TimeSpan.FromSeconds(1);
///
- /// The sound played when extracting
+ /// The sound played when extracting
///
- [DataField("processSound")]
+ [DataField]
public SoundSpecifier? ProcessSound;
public EntityUid? Stream;
///
- /// A minium hunger threshold for extracting nutrition.
- /// Ignored when emagged.
+ /// A minium hunger threshold for extracting nutrition.
+ /// Ignored when emagged.
///
- [DataField("minHungerThreshold")]
+ [DataField]
public HungerThreshold MinHungerThreshold = HungerThreshold.Okay;
}
diff --git a/Content.Server/Nutrition/EntitySystems/FatExtractorSystem.cs b/Content.Server/Nutrition/EntitySystems/FatExtractorSystem.cs
index 180e40d1e42..dc1f67c7400 100644
--- a/Content.Server/Nutrition/EntitySystems/FatExtractorSystem.cs
+++ b/Content.Server/Nutrition/EntitySystems/FatExtractorSystem.cs
@@ -1,5 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
+using Content.Server.Construction;
using Content.Server.Nutrition.Components;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
@@ -9,6 +10,7 @@
using Content.Shared.Nutrition.Components;
using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Storage.Components;
+using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Timing;
@@ -27,12 +29,25 @@ public sealed class FatExtractorSystem : EntitySystem
///
public override void Initialize()
{
+ SubscribeLocalEvent(OnRefreshParts);
+ SubscribeLocalEvent(OnUpgradeExamine);
SubscribeLocalEvent(OnGotEmagged);
SubscribeLocalEvent(OnClosed);
SubscribeLocalEvent(OnOpen);
SubscribeLocalEvent(OnPowerChanged);
}
+ private void OnRefreshParts(EntityUid uid, FatExtractorComponent component, RefreshPartsEvent args)
+ {
+ var rating = args.PartRatings[component.MachinePartNutritionRate] - 1;
+ component.NutritionPerSecond = component.BaseNutritionPerSecond + (int) (component.PartRatingRateMultiplier * rating);
+ }
+
+ private void OnUpgradeExamine(EntityUid uid, FatExtractorComponent component, UpgradeExamineEvent args)
+ {
+ args.AddPercentageUpgrade("fat-extractor-component-rate", (float) component.NutritionPerSecond / component.BaseNutritionPerSecond);
+ }
+
private void OnGotEmagged(EntityUid uid, FatExtractorComponent component, ref GotEmaggedEvent args)
{
args.Handled = true;
diff --git a/Content.Server/Nyanotrasen/Objectives/Components/BecomePsionicConditionComponent.cs b/Content.Server/Nyanotrasen/Objectives/Components/BecomePsionicConditionComponent.cs
deleted file mode 100644
index 3b677bab2d4..00000000000
--- a/Content.Server/Nyanotrasen/Objectives/Components/BecomePsionicConditionComponent.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using Content.Server.Objectives.Systems;
-
-namespace Content.Server.Objectives.Components;
-
-///
-/// Requires that the player dies to be complete.
-///
-[RegisterComponent, Access(typeof(BecomePsionicConditionSystem))]
-public sealed partial class BecomePsionicConditionComponent : Component
-{
-}
\ No newline at end of file
diff --git a/Content.Server/Nyanotrasen/Objectives/Systems/BecomePsionicConditionSystem.cs b/Content.Server/Nyanotrasen/Objectives/Systems/BecomePsionicConditionSystem.cs
deleted file mode 100644
index d090c320a41..00000000000
--- a/Content.Server/Nyanotrasen/Objectives/Systems/BecomePsionicConditionSystem.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-using Content.Shared.Abilities.Psionics;
-using Content.Server.Objectives.Components;
-using Content.Shared.Mind;
-using Content.Shared.Objectives.Components;
-
-namespace Content.Server.Objectives.Systems
-{
- public sealed class BecomePsionicConditionSystem : EntitySystem
- {
- private EntityQuery _metaQuery;
-
- public override void Initialize()
- {
- base.Initialize();
-
- SubscribeLocalEvent(OnGetProgress);
- }
-
- private void OnGetProgress(EntityUid uid, BecomePsionicConditionComponent comp, ref ObjectiveGetProgressEvent args)
- {
- args.Progress = GetProgress(args.Mind);
- }
-
- private float GetProgress(MindComponent mind)
- {
- var entMan = IoCManager.Resolve();
- if (HasComp(mind.CurrentEntity))
- return 1;
- return 0;
- }
- }
-}
diff --git a/Content.Server/Power/Components/UpgradeBatteryComponent.cs b/Content.Server/Power/Components/UpgradeBatteryComponent.cs
new file mode 100644
index 00000000000..b676883b711
--- /dev/null
+++ b/Content.Server/Power/Components/UpgradeBatteryComponent.cs
@@ -0,0 +1,28 @@
+using Content.Shared.Construction.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
+namespace Content.Server.Power.Components
+{
+
+ [RegisterComponent]
+ public sealed partial class UpgradeBatteryComponent : Component
+ {
+ ///
+ /// The machine part that affects the power capacity.
+ ///
+ [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public string MachinePartPowerCapacity = "PowerCell";
+
+ ///
+ /// The machine part rating is raised to this power when calculating power gain
+ ///
+ [DataField]
+ public float MaxChargeMultiplier = 2f;
+
+ ///
+ /// Power gain scaling
+ ///
+ [DataField]
+ public float BaseMaxCharge = 8000000;
+ }
+}
diff --git a/Content.Server/Power/Components/UpgradePowerDrawComponent.cs b/Content.Server/Power/Components/UpgradePowerDrawComponent.cs
new file mode 100644
index 00000000000..23db4905cc5
--- /dev/null
+++ b/Content.Server/Power/Components/UpgradePowerDrawComponent.cs
@@ -0,0 +1,41 @@
+using Content.Server.Construction.Components;
+using Content.Shared.Construction.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
+namespace Content.Server.Power.Components;
+
+///
+/// This is used for machines whose power draw
+/// can be decreased through machine part upgrades.
+///
+[RegisterComponent]
+public sealed partial class UpgradePowerDrawComponent : Component
+{
+ ///
+ /// The base power draw of the machine.
+ /// Prioritizes hv/mv draw over lv draw.
+ /// Value is initializezd on map init from
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float BaseLoad;
+
+ ///
+ /// The machine part that affects the power draw.
+ ///
+ [DataField(customTypeSerializer: typeof(PrototypeIdSerializer)), ViewVariables(VVAccess.ReadWrite)]
+ public string MachinePartPowerDraw = "Capacitor";
+
+ ///
+ /// The multiplier used for scaling the power draw.
+ ///
+ [DataField(required: true), ViewVariables(VVAccess.ReadWrite)]
+ public float PowerDrawMultiplier = 1f;
+
+ ///
+ /// What type of scaling is being used?
+ ///
+ [DataField(required: true), ViewVariables(VVAccess.ReadWrite)]
+ public MachineUpgradeScalingType Scaling;
+}
+
+
diff --git a/Content.Server/Power/Components/UpgradePowerSupplierComponent.cs b/Content.Server/Power/Components/UpgradePowerSupplierComponent.cs
new file mode 100644
index 00000000000..012c38a6b90
--- /dev/null
+++ b/Content.Server/Power/Components/UpgradePowerSupplierComponent.cs
@@ -0,0 +1,36 @@
+using Content.Server.Construction.Components;
+using Content.Shared.Construction.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
+namespace Content.Server.Power.Components;
+
+[RegisterComponent]
+public sealed partial class UpgradePowerSupplierComponent : Component
+{
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float BaseSupplyRate;
+
+ ///
+ /// The machine part that affects the power supplu.
+ ///
+ [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public string MachinePartPowerSupply = "Capacitor";
+
+ ///
+ /// The multiplier used for scaling the power supply.
+ ///
+ [DataField(required: true)]
+ public float PowerSupplyMultiplier = 1f;
+
+ ///
+ /// What type of scaling is being used?
+ ///
+ [DataField(required: true)]
+ public MachineUpgradeScalingType Scaling;
+
+ ///
+ /// The current value that the power supply is being scaled by,
+ ///
+ [DataField]
+ public float ActualScalar = 1f;
+}
diff --git a/Content.Server/Power/Components/UpgradePowerSupplyRampingComponent.cs b/Content.Server/Power/Components/UpgradePowerSupplyRampingComponent.cs
new file mode 100644
index 00000000000..61a654b383b
--- /dev/null
+++ b/Content.Server/Power/Components/UpgradePowerSupplyRampingComponent.cs
@@ -0,0 +1,36 @@
+using Content.Server.Construction.Components;
+using Content.Shared.Construction.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
+namespace Content.Server.Power.Components;
+
+[RegisterComponent]
+public sealed partial class UpgradePowerSupplyRampingComponent : Component
+{
+ [ViewVariables(VVAccess.ReadWrite)]
+ public float BaseRampRate;
+
+ ///
+ /// The machine part that affects the power supply ramping
+ ///
+ [DataField(customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public string MachinePartRampRate = "Capacitor";
+
+ ///
+ /// The multiplier used for scaling the power supply ramping
+ ///
+ [DataField]
+ public float SupplyRampingMultiplier = 1f;
+
+ ///
+ /// What type of scaling is being used?
+ ///
+ [DataField(required: true)]
+ public MachineUpgradeScalingType Scaling;
+
+ ///
+ /// The current value that the power supply is being scaled by
+ ///
+ [DataField]
+ public float ActualScalar = 1f;
+}
diff --git a/Content.Server/Power/EntitySystems/UpgradeBatterySystem.cs b/Content.Server/Power/EntitySystems/UpgradeBatterySystem.cs
new file mode 100644
index 00000000000..734cf9d89ce
--- /dev/null
+++ b/Content.Server/Power/EntitySystems/UpgradeBatterySystem.cs
@@ -0,0 +1,39 @@
+using Content.Server.Construction;
+using Content.Server.Power.Components;
+using JetBrains.Annotations;
+
+namespace Content.Server.Power.EntitySystems
+{
+ [UsedImplicitly]
+ public sealed class UpgradeBatterySystem : EntitySystem
+ {
+ [Dependency] private readonly BatterySystem _batterySystem = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnRefreshParts);
+ SubscribeLocalEvent(OnUpgradeExamine);
+ }
+
+ public void OnRefreshParts(EntityUid uid, UpgradeBatteryComponent component, RefreshPartsEvent args)
+ {
+ var powerCellRating = args.PartRatings[component.MachinePartPowerCapacity];
+
+ if (TryComp(uid, out var batteryComp))
+ {
+ _batterySystem.SetMaxCharge(uid, MathF.Pow(component.MaxChargeMultiplier, powerCellRating - 1) * component.BaseMaxCharge, batteryComp);
+ }
+ }
+
+ private void OnUpgradeExamine(EntityUid uid, UpgradeBatteryComponent component, UpgradeExamineEvent args)
+ {
+ // UpgradeBatteryComponent.MaxChargeMultiplier is not the actual multiplier, so we have to do this.
+ if (TryComp(uid, out var batteryComp))
+ {
+ args.AddPercentageUpgrade("upgrade-max-charge", batteryComp.MaxCharge / component.BaseMaxCharge);
+ }
+ }
+ }
+}
diff --git a/Content.Server/Power/EntitySystems/UpgradePowerSystem.cs b/Content.Server/Power/EntitySystems/UpgradePowerSystem.cs
new file mode 100644
index 00000000000..d2f6ee4f568
--- /dev/null
+++ b/Content.Server/Power/EntitySystems/UpgradePowerSystem.cs
@@ -0,0 +1,151 @@
+using Content.Server.Construction;
+using Content.Server.Construction.Components;
+using Content.Server.Power.Components;
+
+namespace Content.Server.Power.EntitySystems;
+
+///
+/// This handles using upgraded machine parts
+/// to modify the power supply/generation of a machine.
+///
+public sealed class UpgradePowerSystem : EntitySystem
+{
+ ///
+ public override void Initialize()
+ {
+ SubscribeLocalEvent(OnMapInit);
+ SubscribeLocalEvent(OnRefreshParts);
+ SubscribeLocalEvent(OnUpgradeExamine);
+
+ SubscribeLocalEvent(OnSupplierMapInit);
+ SubscribeLocalEvent(OnSupplierRefreshParts);
+ SubscribeLocalEvent