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/Buckle/BuckleSystem.cs b/Content.Client/Buckle/BuckleSystem.cs
index fea18e5cf3c..d4614210d9f 100644
--- a/Content.Client/Buckle/BuckleSystem.cs
+++ b/Content.Client/Buckle/BuckleSystem.cs
@@ -50,17 +50,11 @@ private void OnBuckleAfterAutoHandleState(EntityUid uid, BuckleComponent compone
private void OnAppearanceChange(EntityUid uid, BuckleComponent component, ref AppearanceChangeEvent args)
{
- if (!TryComp(uid, out var rotVisuals))
+ if (!TryComp(uid, out var rotVisuals)
+ || !Appearance.TryGetData(uid, BuckleVisuals.Buckled, out var buckled, args.Component)
+ || !buckled || args.Sprite == null)
return;
- if (!Appearance.TryGetData(uid, BuckleVisuals.Buckled, out var buckled, args.Component) ||
- !buckled ||
- args.Sprite == null)
- {
- _rotationVisualizerSystem.SetHorizontalAngle((uid, rotVisuals), rotVisuals.DefaultRotation);
- return;
- }
-
// Animate strapping yourself to something at a given angle
// TODO: Dump this when buckle is better
_rotationVisualizerSystem.AnimateSpriteRotation(uid, args.Sprite, rotVisuals.HorizontalRotation, 0.125f);
diff --git a/Content.Client/DeltaV/Options/UI/Tabs/DeltaTab.xaml b/Content.Client/DeltaV/Options/UI/Tabs/DeltaTab.xaml
deleted file mode 100644
index f1dae68077d..00000000000
--- a/Content.Client/DeltaV/Options/UI/Tabs/DeltaTab.xaml
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Content.Client/DeltaV/Options/UI/Tabs/DeltaTab.xaml.cs b/Content.Client/DeltaV/Options/UI/Tabs/DeltaTab.xaml.cs
deleted file mode 100644
index 892ddfc15bf..00000000000
--- a/Content.Client/DeltaV/Options/UI/Tabs/DeltaTab.xaml.cs
+++ /dev/null
@@ -1,46 +0,0 @@
-using Content.Shared.DeltaV.CCVars;
-using Robust.Client.AutoGenerated;
-using Robust.Client.UserInterface;
-using Robust.Client.UserInterface.Controls;
-using Robust.Client.UserInterface.XAML;
-using Robust.Shared.Configuration;
-
-namespace Content.Client.DeltaV.Options.UI.Tabs;
-
-[GenerateTypedNameReferences]
-public sealed partial class DeltaTab : Control
-{
- [Dependency] private readonly IConfigurationManager _cfg = default!;
-
- public DeltaTab()
- {
- RobustXamlLoader.Load(this);
- IoCManager.InjectDependencies(this);
-
- DisableFiltersCheckBox.OnToggled += OnCheckBoxToggled;
- DisableFiltersCheckBox.Pressed = _cfg.GetCVar(DCCVars.NoVisionFilters);
-
- ApplyButton.OnPressed += OnApplyButtonPressed;
- UpdateApplyButton();
- }
-
- private void OnCheckBoxToggled(BaseButton.ButtonToggledEventArgs args)
- {
- UpdateApplyButton();
- }
-
- private void OnApplyButtonPressed(BaseButton.ButtonEventArgs args)
- {
- _cfg.SetCVar(DCCVars.NoVisionFilters, DisableFiltersCheckBox.Pressed);
-
- _cfg.SaveToFile();
- UpdateApplyButton();
- }
-
- private void UpdateApplyButton()
- {
- var isNoVisionFiltersSame = DisableFiltersCheckBox.Pressed == _cfg.GetCVar(DCCVars.NoVisionFilters);
-
- ApplyButton.Disabled = isNoVisionFiltersSame;
- }
-}
diff --git a/Content.Client/Flight/Components/FlightVisualsComponent.cs b/Content.Client/Flight/Components/FlightVisualsComponent.cs
new file mode 100644
index 00000000000..3f378f60ef2
--- /dev/null
+++ b/Content.Client/Flight/Components/FlightVisualsComponent.cs
@@ -0,0 +1,40 @@
+using Robust.Client.Graphics;
+using Robust.Shared.GameStates;
+
+namespace Content.Client.Flight.Components;
+
+[RegisterComponent]
+public sealed partial class FlightVisualsComponent : Component
+{
+ ///
+ /// How long does the animation last
+ ///
+ [DataField]
+ public float Speed;
+
+ ///
+ /// How far it goes in any direction.
+ ///
+ [DataField]
+ public float Multiplier;
+
+ ///
+ /// How much the limbs (if there are any) rotate.
+ ///
+ [DataField]
+ public float Offset;
+
+ ///
+ /// Are we animating layers or the entire sprite?
+ ///
+ public bool AnimateLayer = false;
+ public int? TargetLayer;
+
+ [DataField]
+ public string AnimationKey = "default";
+
+ [ViewVariables(VVAccess.ReadWrite)]
+ public ShaderInstance Shader = default!;
+
+
+}
\ No newline at end of file
diff --git a/Content.Client/Flight/FlightSystem.cs b/Content.Client/Flight/FlightSystem.cs
new file mode 100644
index 00000000000..bd1a6767bd9
--- /dev/null
+++ b/Content.Client/Flight/FlightSystem.cs
@@ -0,0 +1,67 @@
+using Robust.Client.GameObjects;
+using Content.Shared.Flight;
+using Content.Shared.Flight.Events;
+using Content.Client.Flight.Components;
+
+namespace Content.Client.Flight;
+public sealed class FlightSystem : SharedFlightSystem
+{
+ [Dependency] private readonly IEntityManager _entityManager = default!;
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeNetworkEvent(OnFlight);
+
+ }
+
+ private void OnFlight(FlightEvent args)
+ {
+ var uid = GetEntity(args.Uid);
+ if (!_entityManager.TryGetComponent(uid, out SpriteComponent? sprite)
+ || !args.IsAnimated
+ || !_entityManager.TryGetComponent(uid, out FlightComponent? flight))
+ return;
+
+
+ int? targetLayer = null;
+ if (flight.IsLayerAnimated && flight.Layer is not null)
+ {
+ targetLayer = GetAnimatedLayer(uid, flight.Layer, sprite);
+ if (targetLayer == null)
+ return;
+ }
+
+ if (args.IsFlying && args.IsAnimated && flight.AnimationKey != "default")
+ {
+ var comp = new FlightVisualsComponent
+ {
+ AnimateLayer = flight.IsLayerAnimated,
+ AnimationKey = flight.AnimationKey,
+ Multiplier = flight.ShaderMultiplier,
+ Offset = flight.ShaderOffset,
+ Speed = flight.ShaderSpeed,
+ TargetLayer = targetLayer,
+ };
+ AddComp(uid, comp);
+ }
+ if (!args.IsFlying)
+ RemComp(uid);
+ }
+
+ public int? GetAnimatedLayer(EntityUid uid, string targetLayer, SpriteComponent? sprite = null)
+ {
+ if (!Resolve(uid, ref sprite))
+ return null;
+
+ int index = 0;
+ foreach (var layer in sprite.AllLayers)
+ {
+ // This feels like absolute shitcode, isn't there a better way to check for it?
+ if (layer.Rsi?.Path.ToString() == targetLayer)
+ return index;
+ index++;
+ }
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/Content.Client/Flight/FlyingVisualizerSystem.cs b/Content.Client/Flight/FlyingVisualizerSystem.cs
new file mode 100644
index 00000000000..6dde6cf5638
--- /dev/null
+++ b/Content.Client/Flight/FlyingVisualizerSystem.cs
@@ -0,0 +1,64 @@
+using Content.Client.Flight.Components;
+using Robust.Client.GameObjects;
+using Robust.Client.Graphics;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.Flight;
+
+///
+/// Handles offsetting an entity while flying
+///
+public sealed class FlyingVisualizerSystem : EntitySystem
+{
+ [Dependency] private readonly IPrototypeManager _protoMan = default!;
+ [Dependency] private readonly SpriteSystem _spriteSystem = default!;
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnStartup);
+ SubscribeLocalEvent(OnShutdown);
+ SubscribeLocalEvent(OnBeforeShaderPost);
+ }
+
+ private void OnStartup(EntityUid uid, FlightVisualsComponent comp, ComponentStartup args)
+ {
+ comp.Shader = _protoMan.Index(comp.AnimationKey).InstanceUnique();
+ AddShader(uid, comp.Shader, comp.AnimateLayer, comp.TargetLayer);
+ SetValues(comp, comp.Speed, comp.Offset, comp.Multiplier);
+ }
+
+ private void OnShutdown(EntityUid uid, FlightVisualsComponent comp, ComponentShutdown args)
+ {
+ AddShader(uid, null, comp.AnimateLayer, comp.TargetLayer);
+ }
+
+ private void AddShader(Entity entity, ShaderInstance? shader, bool animateLayer, int? layer)
+ {
+ if (!Resolve(entity, ref entity.Comp, false))
+ return;
+
+ if (!animateLayer)
+ entity.Comp.PostShader = shader;
+
+ if (animateLayer && layer is not null)
+ entity.Comp.LayerSetShader(layer.Value, shader);
+
+ entity.Comp.GetScreenTexture = shader is not null;
+ entity.Comp.RaiseShaderEvent = shader is not null;
+ }
+
+ ///
+ /// This function can be used to modify the shader's values while its running.
+ ///
+ private void OnBeforeShaderPost(EntityUid uid, FlightVisualsComponent comp, ref BeforePostShaderRenderEvent args)
+ {
+ SetValues(comp, comp.Speed, comp.Offset, comp.Multiplier);
+ }
+
+ private void SetValues(FlightVisualsComponent comp, float speed, float offset, float multiplier)
+ {
+ comp.Shader.SetParameter("Speed", speed);
+ comp.Shader.SetParameter("Offset", offset);
+ comp.Shader.SetParameter("Multiplier", multiplier);
+ }
+}
\ No newline at end of file
diff --git a/Content.Client/Input/ContentContexts.cs b/Content.Client/Input/ContentContexts.cs
index a895cdd0093..dfc00eff88b 100644
--- a/Content.Client/Input/ContentContexts.cs
+++ b/Content.Client/Input/ContentContexts.cs
@@ -84,6 +84,7 @@ public static void SetupContexts(IInputContextContainer contexts)
human.AddFunction(ContentKeyFunctions.Arcade1);
human.AddFunction(ContentKeyFunctions.Arcade2);
human.AddFunction(ContentKeyFunctions.Arcade3);
+ human.AddFunction(ContentKeyFunctions.LookUp);
// actions should be common (for ghosts, mobs, etc)
common.AddFunction(ContentKeyFunctions.OpenActionsMenu);
diff --git a/Content.Client/Options/UI/OptionsMenu.xaml b/Content.Client/Options/UI/OptionsMenu.xaml
index 69daaa2cea7..ab3b88ca4e6 100644
--- a/Content.Client/Options/UI/OptionsMenu.xaml
+++ b/Content.Client/Options/UI/OptionsMenu.xaml
@@ -1,6 +1,5 @@
@@ -9,6 +8,5 @@
-
diff --git a/Content.Client/Options/UI/OptionsMenu.xaml.cs b/Content.Client/Options/UI/OptionsMenu.xaml.cs
index bb2c1ce0ed9..c3a8e664705 100644
--- a/Content.Client/Options/UI/OptionsMenu.xaml.cs
+++ b/Content.Client/Options/UI/OptionsMenu.xaml.cs
@@ -20,7 +20,6 @@ public OptionsMenu()
Tabs.SetTabTitle(2, Loc.GetString("ui-options-tab-controls"));
Tabs.SetTabTitle(3, Loc.GetString("ui-options-tab-audio"));
Tabs.SetTabTitle(4, Loc.GetString("ui-options-tab-network"));
- Tabs.SetTabTitle(5, Loc.GetString("ui-options-tab-deltav")); // DeltaV specific settings
UpdateTabs();
}
diff --git a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs
index bc8239ee9e6..1b2d3ff7477 100644
--- a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs
+++ b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs
@@ -97,6 +97,12 @@ private void HandleToggleWalk(BaseButton.ButtonToggledEventArgs args)
_deferCommands.Add(_inputManager.SaveToUserData);
}
+ private void HandleHoldLookUp(BaseButton.ButtonToggledEventArgs args)
+ {
+ _cfg.SetCVar(CCVars.HoldLookUp, args.Pressed);
+ _cfg.SaveToFile();
+ }
+
private void HandleDefaultWalk(BaseButton.ButtonToggledEventArgs args)
{
_cfg.SetCVar(CCVars.DefaultWalk, args.Pressed);
@@ -109,6 +115,12 @@ private void HandleStaticStorageUI(BaseButton.ButtonToggledEventArgs args)
_cfg.SaveToFile();
}
+ private void HandleToggleAutoGetUp(BaseButton.ButtonToggledEventArgs args)
+ {
+ _cfg.SetCVar(CCVars.AutoGetUp, args.Pressed);
+ _cfg.SaveToFile();
+ }
+
public KeyRebindTab()
{
IoCManager.InjectDependencies(this);
@@ -193,6 +205,9 @@ void AddCheckBox(string checkBoxName, bool currentState, Action
+
(OnPlayerAttached);
SubscribeLocalEvent(OnPlayerDetached);
- Subs.CVar(_cfg, DCCVars.NoVisionFilters, OnNoVisionFiltersChanged);
+ Subs.CVar(_cfg, CCVars.NoVisionFilters, OnNoVisionFiltersChanged);
_overlay = new();
}
@@ -33,7 +33,7 @@ private void OnDogVisionInit(EntityUid uid, DogVisionComponent component, Compon
if (uid != _playerMan.LocalEntity)
return;
- if (!_cfg.GetCVar(DCCVars.NoVisionFilters))
+ if (!_cfg.GetCVar(CCVars.NoVisionFilters))
_overlayMan.AddOverlay(_overlay);
}
@@ -47,7 +47,7 @@ private void OnDogVisionShutdown(EntityUid uid, DogVisionComponent component, Co
private void OnPlayerAttached(EntityUid uid, DogVisionComponent component, LocalPlayerAttachedEvent args)
{
- if (!_cfg.GetCVar(DCCVars.NoVisionFilters))
+ if (!_cfg.GetCVar(CCVars.NoVisionFilters))
_overlayMan.AddOverlay(_overlay);
}
diff --git a/Content.Client/Overlays/UltraVisionSystem.cs b/Content.Client/Overlays/UltraVisionSystem.cs
index 7728a647848..e8e6cdfa72b 100644
--- a/Content.Client/Overlays/UltraVisionSystem.cs
+++ b/Content.Client/Overlays/UltraVisionSystem.cs
@@ -1,5 +1,5 @@
using Content.Shared.Traits.Assorted.Components;
-using Content.Shared.DeltaV.CCVars;
+using Content.Shared.CCVar;
using Robust.Client.Graphics;
using Robust.Shared.Configuration;
using Robust.Shared.Player;
@@ -23,7 +23,7 @@ public override void Initialize()
SubscribeLocalEvent(OnPlayerAttached);
SubscribeLocalEvent(OnPlayerDetached);
- Subs.CVar(_cfg, DCCVars.NoVisionFilters, OnNoVisionFiltersChanged);
+ Subs.CVar(_cfg, CCVars.NoVisionFilters, OnNoVisionFiltersChanged);
_overlay = new();
}
@@ -33,7 +33,7 @@ private void OnUltraVisionInit(EntityUid uid, UltraVisionComponent component, Co
if (uid != _playerMan.LocalEntity)
return;
- if (!_cfg.GetCVar(DCCVars.NoVisionFilters))
+ if (!_cfg.GetCVar(CCVars.NoVisionFilters))
_overlayMan.AddOverlay(_overlay);
}
@@ -47,7 +47,7 @@ private void OnUltraVisionShutdown(EntityUid uid, UltraVisionComponent component
private void OnPlayerAttached(EntityUid uid, UltraVisionComponent component, LocalPlayerAttachedEvent args)
{
- if (!_cfg.GetCVar(DCCVars.NoVisionFilters))
+ if (!_cfg.GetCVar(CCVars.NoVisionFilters))
_overlayMan.AddOverlay(_overlay);
}
diff --git a/Content.Client/Standing/LayingDownSystem.cs b/Content.Client/Standing/LayingDownSystem.cs
new file mode 100644
index 00000000000..3a1f438df05
--- /dev/null
+++ b/Content.Client/Standing/LayingDownSystem.cs
@@ -0,0 +1,97 @@
+using Content.Shared.Buckle;
+using Content.Shared.Rotation;
+using Content.Shared.Standing;
+using Robust.Client.GameObjects;
+using Robust.Client.Graphics;
+using Robust.Shared.Timing;
+using DrawDepth = Content.Shared.DrawDepth.DrawDepth;
+
+namespace Content.Client.Standing;
+
+public sealed class LayingDownSystem : SharedLayingDownSystem
+{
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly IEyeManager _eyeManager = default!;
+ [Dependency] private readonly StandingStateSystem _standing = default!;
+ [Dependency] private readonly AnimationPlayerSystem _animation = default!;
+ [Dependency] private readonly SharedBuckleSystem _buckle = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnMovementInput);
+ SubscribeNetworkEvent(OnDowned);
+ SubscribeLocalEvent(OnStood);
+
+ SubscribeNetworkEvent(OnCheckAutoGetUp);
+ }
+
+ private void OnMovementInput(EntityUid uid, LayingDownComponent component, MoveEvent args)
+ {
+ if (!_timing.IsFirstTimePredicted
+ || !_standing.IsDown(uid)
+ || _buckle.IsBuckled(uid)
+ || _animation.HasRunningAnimation(uid, "rotate")
+ || !TryComp(uid, out var transform)
+ || !TryComp(uid, out var sprite)
+ || !TryComp(uid, out var rotationVisuals))
+ return;
+
+ var rotation = transform.LocalRotation + (_eyeManager.CurrentEye.Rotation - (transform.LocalRotation - transform.WorldRotation));
+
+ if (rotation.GetDir() is Direction.SouthEast or Direction.East or Direction.NorthEast or Direction.North)
+ {
+ rotationVisuals.HorizontalRotation = Angle.FromDegrees(270);
+ sprite.Rotation = Angle.FromDegrees(270);
+ return;
+ }
+
+ rotationVisuals.HorizontalRotation = Angle.FromDegrees(90);
+ 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)
+ return;
+
+ var uid = GetEntity(ev.User);
+
+ if (!TryComp(uid, out var transform) || !TryComp(uid, out var rotationVisuals))
+ return;
+
+ var rotation = transform.LocalRotation + (_eyeManager.CurrentEye.Rotation - (transform.LocalRotation - transform.WorldRotation));
+
+ if (rotation.GetDir() is Direction.SouthEast or Direction.East or Direction.NorthEast or Direction.North)
+ {
+ rotationVisuals.HorizontalRotation = Angle.FromDegrees(270);
+ return;
+ }
+
+ rotationVisuals.HorizontalRotation = Angle.FromDegrees(90);
+ }
+}
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.Client/Telescope/TelescopeSystem.cs b/Content.Client/Telescope/TelescopeSystem.cs
new file mode 100644
index 00000000000..ac2270aa971
--- /dev/null
+++ b/Content.Client/Telescope/TelescopeSystem.cs
@@ -0,0 +1,128 @@
+using System.Numerics;
+using Content.Client.Viewport;
+using Content.Shared.CCVar;
+using Content.Shared.Telescope;
+using Content.Shared.Input;
+using Robust.Client.GameObjects;
+using Robust.Client.Graphics;
+using Robust.Client.Input;
+using Robust.Client.Player;
+using Robust.Client.UserInterface;
+using Robust.Shared.Configuration;
+using Robust.Shared.Input;
+using Robust.Shared.Input.Binding;
+using Robust.Shared.Timing;
+
+namespace Content.Client.Telescope;
+
+public sealed class TelescopeSystem : SharedTelescopeSystem
+{
+ [Dependency] private readonly InputSystem _inputSystem = default!;
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly IPlayerManager _player = default!;
+ [Dependency] private readonly IInputManager _input = default!;
+ [Dependency] private readonly IEyeManager _eyeManager = default!;
+ [Dependency] private readonly IUserInterfaceManager _uiManager = default!;
+ [Dependency] private readonly IConfigurationManager _cfg = default!;
+
+ private ScalingViewport? _viewport;
+ private bool _holdLookUp;
+ private bool _toggled;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ _cfg.OnValueChanged(CCVars.HoldLookUp,
+ val =>
+ {
+ var input = val ? null : InputCmdHandler.FromDelegate(_ => _toggled = !_toggled);
+ _input.SetInputCommand(ContentKeyFunctions.LookUp, input);
+ _holdLookUp = val;
+ _toggled = false;
+ },
+ true);
+ }
+
+ public override void FrameUpdate(float frameTime)
+ {
+ base.FrameUpdate(frameTime);
+
+ if (_timing.ApplyingState
+ || !_timing.IsFirstTimePredicted
+ || !_input.MouseScreenPosition.IsValid)
+ return;
+
+ var player = _player.LocalEntity;
+
+ var telescope = GetRightTelescope(player);
+
+ if (telescope == null)
+ {
+ _toggled = false;
+ return;
+ }
+
+ if (!TryComp(player, out var eye))
+ return;
+
+ var offset = Vector2.Zero;
+
+ if (_holdLookUp)
+ {
+ if (_inputSystem.CmdStates.GetState(ContentKeyFunctions.LookUp) != BoundKeyState.Down)
+ {
+ RaiseEvent(offset);
+ return;
+ }
+ }
+ else if (!_toggled)
+ {
+ RaiseEvent(offset);
+ return;
+ }
+
+ var mousePos = _input.MouseScreenPosition;
+
+ if (_uiManager.MouseGetControl(mousePos) as ScalingViewport is { } viewport)
+ _viewport = viewport;
+
+ if (_viewport == null)
+ return;
+
+ var centerPos = _eyeManager.WorldToScreen(eye.Eye.Position.Position + eye.Offset);
+
+ var diff = mousePos.Position - centerPos;
+ var len = diff.Length();
+
+ var size = _viewport.PixelSize;
+
+ var maxLength = Math.Min(size.X, size.Y) * 0.4f;
+ var minLength = maxLength * 0.2f;
+
+ if (len > maxLength)
+ {
+ diff *= maxLength / len;
+ len = maxLength;
+ }
+
+ var divisor = maxLength * telescope.Divisor;
+
+ if (len > minLength)
+ {
+ diff -= diff * minLength / len;
+ offset = new Vector2(diff.X / divisor, -diff.Y / divisor);
+ offset = new Angle(-eye.Rotation.Theta).RotateVec(offset);
+ }
+
+ RaiseEvent(offset);
+ }
+
+ private void RaiseEvent(Vector2 offset)
+ {
+ RaisePredictiveEvent(new EyeOffsetChangedEvent
+ {
+ Offset = offset
+ });
+ }
+}
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/Abilities/Psionics/PsionicAbilitiesSystem.cs b/Content.Server/Abilities/Psionics/PsionicAbilitiesSystem.cs
index 32e51d3c101..536235b6d63 100644
--- a/Content.Server/Abilities/Psionics/PsionicAbilitiesSystem.cs
+++ b/Content.Server/Abilities/Psionics/PsionicAbilitiesSystem.cs
@@ -12,6 +12,7 @@
using System.Linq;
using Robust.Server.Player;
using Content.Server.Chat.Managers;
+using Content.Server.Psionics.Glimmer;
namespace Content.Server.Abilities.Psionics
{
@@ -122,6 +123,8 @@ public void InitializePsionicPower(EntityUid uid, PsionicPowerPrototype proto, P
AddPsionicStatSources(proto, psionic);
RefreshPsionicModifiers(uid, psionic);
SendFeedbackMessage(uid, proto, playFeedback);
+ UpdatePowerSlots(psionic);
+ //UpdatePsionicDanger(uid, psionic); // TODO: After Glimmer Refactor
//SendFeedbackAudio(uid, proto, playPopup); // TODO: This one is coming next!
}
@@ -297,6 +300,27 @@ private void SendFeedbackMessage(EntityUid uid, PsionicPowerPrototype proto, boo
session.Channel);
}
+ private void UpdatePowerSlots(PsionicComponent psionic)
+ {
+ var slotsUsed = 0;
+ foreach (var power in psionic.ActivePowers)
+ slotsUsed += power.PowerSlotCost;
+
+ psionic.PowerSlotsTaken = slotsUsed;
+ }
+
+ ///
+ /// Psions over a certain power threshold become a glimmer source. This cannot be fully implemented until after I rework Glimmer
+ ///
+ //private void UpdatePsionicDanger(EntityUid uid, PsionicComponent psionic)
+ //{
+ // if (psionic.PowerSlotsTaken <= psionic.PowerSlots)
+ // return;
+ //
+ // EnsureComp(uid, out var glimmerSource);
+ // glimmerSource.SecondsPerGlimmer = 10 / (psionic.PowerSlotsTaken - psionic.PowerSlots);
+ //}
+
///
/// Remove all Psychic Actions listed in an entity's Psionic Component. Unfortunately, removing actions associated with a specific Power Prototype is not supported.
///
diff --git a/Content.Server/Administration/Commands/SetOutfitCommand.cs b/Content.Server/Administration/Commands/SetOutfitCommand.cs
index 2f979f4340b..e19c5b72fa4 100644
--- a/Content.Server/Administration/Commands/SetOutfitCommand.cs
+++ b/Content.Server/Administration/Commands/SetOutfitCommand.cs
@@ -13,6 +13,8 @@
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Content.Server.Silicon.IPC;
+using Content.Shared.Radio.Components;
+using Content.Shared.Cluwne;
namespace Content.Server.Administration.Commands
{
@@ -127,7 +129,14 @@ public static bool SetOutfit(EntityUid target, string gear, IEntityManager entit
handsSystem.TryPickup(target, inhandEntity, checkActionBlocker: false, handsComp: handsComponent);
}
}
- InternalEncryptionKeySpawner.TryInsertEncryptionKey(target, startingGear, entityManager, profile);
+
+ 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();
+ encryption.TryInsertEncryptionKey(target, startingGear, entityManager);
+ }
return true;
}
}
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 881f54512a1..04a9023c1dd 100644
--- a/Content.Server/Atmos/Monitor/Systems/AirAlarmSystem.cs
+++ b/Content.Server/Atmos/Monitor/Systems/AirAlarmSystem.cs
@@ -586,6 +586,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 bc5db2e22cb..fbe40deedb1 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.Atmos;
using Content.Shared.Database;
@@ -39,6 +40,8 @@ public override void Initialize()
SubscribeLocalEvent(OnExamined);
SubscribeLocalEvent(OnDestroyed);
SubscribeLocalEvent(OnScrubberAnalyzed);
+ SubscribeLocalEvent(OnRefreshParts);
+ SubscribeLocalEvent(OnUpgradeExamine);
}
private bool IsFull(PortableScrubberComponent component)
@@ -155,5 +158,20 @@ private void OnScrubberAnalyzed(EntityUid uid, PortableScrubberComponent compone
args.GasMixtures ??= new List<(string, GasMixture?)>();
args.GasMixtures.Add((Name(uid), component.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 f83ec1a5123..223dd2ac329 100644
--- a/Content.Server/Cargo/Systems/CargoSystem.Telepad.cs
+++ b/Content.Server/Cargo/Systems/CargoSystem.Telepad.cs
@@ -1,5 +1,7 @@
using System.Linq;
using Content.Server.Cargo.Components;
+using Content.Server.Construction;
+using Content.Server.Paper;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Server.Station.Components;
@@ -17,6 +19,8 @@ public sealed partial class CargoSystem
private void InitializeTelepad()
{
SubscribeLocalEvent(OnInit);
+ SubscribeLocalEvent(OnRefreshParts);
+ SubscribeLocalEvent(OnUpgradeExamine);
SubscribeLocalEvent(OnShutdown);
SubscribeLocalEvent(OnTelepadPowerChange);
// Shouldn't need re-anchored event
@@ -110,6 +114,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 OnShutdown(Entity ent, ref ComponentShutdown args)
{
if (ent.Comp.CurrentOrders.Count == 0)
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.Utility.cs b/Content.Server/Cloning/CloningSystem.Utility.cs
index 408e1cf24a3..aa2ce27902c 100644
--- a/Content.Server/Cloning/CloningSystem.Utility.cs
+++ b/Content.Server/Cloning/CloningSystem.Utility.cs
@@ -68,9 +68,10 @@ private bool CheckUncloneable(EntityUid uid, EntityUid bodyToClone, CloningPodCo
if (ev.Cancelled && ev.CloningFailMessage is not null)
{
- _chatSystem.TrySendInGameICMessage(uid,
- Loc.GetString(ev.CloningFailMessage),
- InGameICChatType.Speak, false);
+ if (clonePod.ConnectedConsole is not null)
+ _chatSystem.TrySendInGameICMessage(clonePod.ConnectedConsole.Value,
+ Loc.GetString(ev.CloningFailMessage),
+ InGameICChatType.Speak, false);
return false;
}
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
new file mode 100644
index 00000000000..39321b1e66c
--- /dev/null
+++ b/Content.Server/Flight/FlightSystem.cs
@@ -0,0 +1,176 @@
+
+using Content.Shared.Cuffs.Components;
+using Content.Shared.Damage.Components;
+using Content.Shared.DoAfter;
+using Content.Shared.Flight;
+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;
+
+namespace Content.Server.Flight;
+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()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnToggleFlight);
+ SubscribeLocalEvent(OnFlightDoAfter);
+ SubscribeLocalEvent(OnMobStateChangedEvent);
+ SubscribeLocalEvent(OnZombified);
+ SubscribeLocalEvent(OnKnockedDown);
+ SubscribeLocalEvent(OnStunned);
+ SubscribeLocalEvent(OnDowned);
+ SubscribeLocalEvent(OnSleep);
+ }
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var uid, out var component))
+ {
+ if (!component.On)
+ continue;
+
+ component.TimeUntilFlap -= frameTime;
+
+ if (component.TimeUntilFlap > 0f)
+ continue;
+
+ _audio.PlayPvs(component.FlapSound, uid);
+ component.TimeUntilFlap = component.FlapInterval;
+
+ }
+ }
+
+ #region Core Functions
+ private void OnToggleFlight(EntityUid uid, FlightComponent component, ToggleFlightEvent args)
+ {
+ // If the user isnt flying, we check for conditionals and initiate a doafter.
+ if (!component.On)
+ {
+ if (!CanFly(uid, component))
+ return;
+
+ var doAfterArgs = new DoAfterArgs(EntityManager,
+ uid, component.ActivationDelay,
+ new FlightDoAfterEvent(), uid, target: uid)
+ {
+ BlockDuplicate = true,
+ BreakOnTargetMove = true,
+ BreakOnUserMove = true,
+ BreakOnDamage = true,
+ NeedHand = true
+ };
+
+ if (!_doAfter.TryStartDoAfter(doAfterArgs))
+ return;
+ }
+ else
+ ToggleActive(uid, false, component);
+ }
+
+ private void OnFlightDoAfter(EntityUid uid, FlightComponent component, FlightDoAfterEvent args)
+ {
+ if (args.Handled || args.Cancelled)
+ return;
+
+ ToggleActive(uid, true, component);
+ args.Handled = true;
+ }
+
+ #endregion
+
+ #region Conditionals
+
+ private bool CanFly(EntityUid uid, FlightComponent component)
+ {
+ if (TryComp(uid, out var cuffableComp) && !cuffableComp.CanStillInteract)
+ {
+ _popupSystem.PopupEntity(Loc.GetString("no-flight-while-restrained"), uid, uid, PopupType.Medium);
+ return false;
+ }
+
+ if (HasComp(uid))
+ {
+ _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;
+ }
+
+ private void OnMobStateChangedEvent(EntityUid uid, FlightComponent component, MobStateChangedEvent args)
+ {
+ if (!component.On
+ || args.NewMobState is MobState.Critical or MobState.Dead)
+ return;
+
+ ToggleActive(args.Target, false, component);
+ }
+
+ private void OnZombified(EntityUid uid, FlightComponent component, ref EntityZombifiedEvent args)
+ {
+ if (!component.On)
+ return;
+
+ ToggleActive(args.Target, false, component);
+ if (!TryComp(uid, out var stamina))
+ return;
+ Dirty(uid, stamina);
+ }
+
+ private void OnKnockedDown(EntityUid uid, FlightComponent component, ref KnockedDownEvent args)
+ {
+ if (!component.On)
+ return;
+
+ ToggleActive(uid, false, component);
+ }
+
+ private void OnStunned(EntityUid uid, FlightComponent component, ref StunnedEvent args)
+ {
+ if (!component.On)
+ return;
+
+ 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
+ || !args.FellAsleep)
+ return;
+
+ ToggleActive(uid, false, component);
+ if (!TryComp(uid, out var stamina))
+ return;
+
+ Dirty(uid, stamina);
+ }
+ #endregion
+}
\ No newline at end of file
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 2d928b2e3f2..dc31c004c64 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);
@@ -254,6 +256,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 8938aa8b1dc..f9e1ae22416 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);
@@ -345,6 +347,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 81001f0932e..93a15f319d9 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