diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 94d30a6d5f5..2e90f0eed03 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,8 +1,20 @@ # Last match in file takes precedence. -/Content.*/SimpleStation14/ @DEATHB4DEFEAT +# C# code +/Content.*/ @DeltaV-Station/maintainers -/Resources/*.yml @Colin-Tel -/Resources/*/SimpleStation14/ @DEATHB4DEFEAT -/Resources/Maps/ @IamVelcroboy -/Resources/Prototypes/Maps/ @IamVelcroboy +# YML files +/Resources/*.yml @DeltaV-Station/yaml-maintainers +/Resources/**/*.yml @DeltaV-Station/yaml-maintainers + +# Sprites +/Resources/Textures/ @IamVelcroboy + +# Lobby art and music - automatically direction issues since its immediately visible to players +/Resources/Audio/Lobby/ @DeltaV-Station/game-directors +/Resources/Textures/LobbyScreens/ @DeltaV-Station/game-directors + +# Maps +/Resources/Maps/ @DeltaV-Station/maptainers +/Resources/Prototypes/Maps/ @DeltaV-Station/maptainers +/Content.IntegrationTests/Tests/PostMapInitTest.cs @DeltaV-Station/maptainers diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 7a8129df1a6..b85b383ee3a 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,47 +1,32 @@ - - + ## About the PR - + ## Why / Balance - + ## Technical details - + ## Media - + ## Requirements - -- [ ] I have read and I am following the [Pull Request Guidelines](https://docs.spacestation14.com/en/general-development/codebase-info/pull-request-guidelines.html). I understand that not doing so may get my pr closed at maintainer’s discretion -- [ ] I have added screenshots/videos to this PR showcasing its changes ingame, **or** this PR does not require an ingame showcase + +- [ ] I have tested all added content and changes. +- [ ] I have added media to this PR or it does not require an ingame showcase. + ## Breaking changes - + **Changelog** + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Content.Client/Atmos/Consoles/AtmosAlarmEntryContainer.xaml.cs b/Content.Client/Atmos/Consoles/AtmosAlarmEntryContainer.xaml.cs new file mode 100644 index 00000000000..79bb66560e3 --- /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; + } +} 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..81c9a409a3b --- /dev/null +++ b/Content.Client/Atmos/Consoles/AtmosAlertsComputerWindow.xaml.cs @@ -0,0 +1,611 @@ +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 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 readonly SharedNavMapSystem _navMapSystem; + + 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; + + // Colors + private Color _wallColor = new Color(64, 64, 64); + private Color _tileColor = new Color(28, 28, 28); + private Color _monitorBlipColor = Color.Cyan; + private Color _untrackedEntColor = Color.DimGray; + private Color _regionBaseColor = new Color(154, 154, 154); + private Color _inactiveColor = StyleNano.DisabledFore; + private Color _statusTextColor = StyleNano.GoodGreenFore; + private Color _goodColor = Color.LimeGreen; + private Color _warningColor = new Color(255, 182, 72); + private Color _dangerColor = new Color(255, 67, 67); + + public AtmosAlertsComputerWindow(AtmosAlertsComputerBoundUserInterface userInterface, EntityUid? owner) + { + RobustXamlLoader.Load(this); + _entManager = IoCManager.Resolve(); + _spriteSystem = _entManager.System(); + _navMapSystem = _entManager.System(); + + // Pass the owner to nav map + _owner = owner; + NavMap.Owner = _owner; + + // Set nav map colors + NavMap.WallColor = _wallColor; + NavMap.TileColor = _tileColor; + + // 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.TryAddMarkup(Loc.GetString("atmos-alerts-window-station-name", ("stationName", stationName)), out _); + + 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 (!device.NetEntity.Valid) + continue; + + 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, _monitorBlipColor, 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", _statusTextColor.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))); + + // Update sensor regions + NavMap.RegionOverlays.Clear(); + var prioritizedRegionOverlays = new Dictionary(); + + if (_owner != null && + _entManager.TryGetComponent(_owner, out var xform) && + _entManager.TryGetComponent(xform.GridUid, out var navMap)) + { + var regionOverlays = _navMapSystem.GetNavMapRegionOverlays(_owner.Value, navMap, AtmosAlertsComputerUiKey.Key); + + foreach (var (regionOwner, regionOverlay) in regionOverlays) + { + var alarmState = GetAlarmState(regionOwner); + + if (!TryGetSensorRegionColor(regionOwner, alarmState, out var regionColor)) + continue; + + regionOverlay.Color = regionColor; + + var priority = (_trackedEntity == regionOwner) ? 999 : (int)alarmState; + prioritizedRegionOverlays.Add(regionOverlay, priority); + } + + // Sort overlays according to their priority + var sortedOverlays = prioritizedRegionOverlays.OrderBy(x => x.Value).Select(x => x.Key).ToList(); + NavMap.RegionOverlays = sortedOverlays; + } + + // 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 *= _untrackedEntColor; + + var selectable = true; + var blip = new NavMapBlip(coords, _spriteSystem.Frame0(texture), color, _trackedEntity == metaData.NetEntity, selectable); + + NavMap.TrackedEntities[metaData.NetEntity] = blip; + } + + private bool TryGetSensorRegionColor(NetEntity regionOwner, AtmosAlarmType alarmState, out Color color) + { + color = Color.White; + + var blip = GetBlipTexture(alarmState); + + if (blip == null) + return false; + + // Color the region based on alarm state and entity tracking + color = blip.Value.Item2 * _regionBaseColor; + + if (_trackedEntity != null && _trackedEntity != regionOwner) + color *= _untrackedEntColor; + + return true; + } + + 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")), _inactiveColor); break; + case AtmosAlarmType.Normal: + output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png")), _goodColor); break; + case AtmosAlarmType.Warning: + output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_triangle.png")), _warningColor); break; + case AtmosAlarmType.Danger: + output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_square.png")), _dangerColor); break; + } + + return output; + } +} diff --git a/Content.Client/Audio/AmbientSoundSystem.cs b/Content.Client/Audio/AmbientSoundSystem.cs index ca6336b91b8..b525747aa9c 100644 --- a/Content.Client/Audio/AmbientSoundSystem.cs +++ b/Content.Client/Audio/AmbientSoundSystem.cs @@ -306,6 +306,9 @@ private void ProcessNearbyAmbience(TransformComponent playerXform) .WithMaxDistance(comp.Range); var stream = _audio.PlayEntity(comp.Sound, Filter.Local(), uid, false, audioParams); + if (stream == null) + continue; + _playingSounds[sourceEntity] = (stream.Value.Entity, comp.Sound, key); playingCount++; diff --git a/Content.Client/Audio/ClientGlobalSoundSystem.cs b/Content.Client/Audio/ClientGlobalSoundSystem.cs index 7c77865f741..50c3971d95a 100644 --- a/Content.Client/Audio/ClientGlobalSoundSystem.cs +++ b/Content.Client/Audio/ClientGlobalSoundSystem.cs @@ -67,7 +67,7 @@ private void PlayAdminSound(AdminSoundEvent soundEvent) if(!_adminAudioEnabled) return; var stream = _audio.PlayGlobal(soundEvent.Filename, Filter.Local(), false, soundEvent.AudioParams); - _adminAudio.Add(stream.Value.Entity); + _adminAudio.Add(stream?.Entity); } private void PlayStationEventMusic(StationEventMusicEvent soundEvent) @@ -76,7 +76,7 @@ private void PlayStationEventMusic(StationEventMusicEvent soundEvent) if(!_eventAudioEnabled || _eventAudio.ContainsKey(soundEvent.Type)) return; var stream = _audio.PlayGlobal(soundEvent.Filename, Filter.Local(), false, soundEvent.AudioParams); - _eventAudio.Add(soundEvent.Type, stream.Value.Entity); + _eventAudio.Add(soundEvent.Type, stream?.Entity); } private void PlayGameSound(GameGlobalSoundEvent soundEvent) diff --git a/Content.Client/Audio/ContentAudioSystem.AmbientMusic.cs b/Content.Client/Audio/ContentAudioSystem.AmbientMusic.cs index d60c978ccf5..bf7ab26cba2 100644 --- a/Content.Client/Audio/ContentAudioSystem.AmbientMusic.cs +++ b/Content.Client/Audio/ContentAudioSystem.AmbientMusic.cs @@ -213,9 +213,9 @@ private void UpdateAmbientMusic() false, AudioParams.Default.WithVolume(_musicProto.Sound.Params.Volume + _volumeSlider)); - _ambientMusicStream = strim.Value.Entity; + _ambientMusicStream = strim?.Entity; - if (_musicProto.FadeIn) + if (_musicProto.FadeIn && strim != null) { FadeIn(_ambientMusicStream, strim.Value.Component, AmbientMusicFadeTime); } diff --git a/Content.Client/Audio/ContentAudioSystem.LobbyMusic.cs b/Content.Client/Audio/ContentAudioSystem.LobbyMusic.cs index 92c5b7a4191..7d7d77f51a3 100644 --- a/Content.Client/Audio/ContentAudioSystem.LobbyMusic.cs +++ b/Content.Client/Audio/ContentAudioSystem.LobbyMusic.cs @@ -20,7 +20,6 @@ public sealed partial class ContentAudioSystem { [Dependency] private readonly IBaseClient _client = default!; [Dependency] private readonly ClientGameTicker _gameTicker = default!; - [Dependency] private readonly IStateManager _stateManager = default!; [Dependency] private readonly IResourceCache _resourceCache = default!; private readonly AudioParams _lobbySoundtrackParams = new(-5f, 1, 0, 0, 0, false, 0f); @@ -71,7 +70,7 @@ private void InitializeLobbyMusic() Subs.CVar(_configManager, CCVars.LobbyMusicEnabled, LobbyMusicCVarChanged); Subs.CVar(_configManager, CCVars.LobbyMusicVolume, LobbyMusicVolumeCVarChanged); - _stateManager.OnStateChanged += StateManagerOnStateChanged; + _state.OnStateChanged += StateManagerOnStateChanged; _client.PlayerLeaveServer += OnLeave; @@ -115,7 +114,7 @@ private void LobbyMusicVolumeCVarChanged(float volume) private void LobbyMusicCVarChanged(bool musicEnabled) { - if (musicEnabled && _stateManager.CurrentState is LobbyState) + if (musicEnabled && _state.CurrentState is LobbyState) { StartLobbyMusic(); } @@ -185,7 +184,7 @@ private void PlaySoundtrack(string soundtrackFilename) false, _lobbySoundtrackParams.WithVolume(_lobbySoundtrackParams.Volume + SharedAudioSystem.GainToVolume(_configManager.GetCVar(CCVars.LobbyMusicVolume))) ); - if (playResult.Value.Entity == default) + if (playResult == null) { _sawmill.Warning( $"Tried to play lobby soundtrack '{{Filename}}' using {nameof(SharedAudioSystem)}.{nameof(SharedAudioSystem.PlayGlobal)} but it returned default value of EntityUid!", @@ -234,7 +233,7 @@ private void PlayRestartSound(RoundRestartCleanupEvent ev) private void ShutdownLobbyMusic() { - _stateManager.OnStateChanged -= StateManagerOnStateChanged; + _state.OnStateChanged -= StateManagerOnStateChanged; _client.PlayerLeaveServer -= OnLeave; diff --git a/Content.Client/Buckle/BuckleSystem.cs b/Content.Client/Buckle/BuckleSystem.cs index 6770899e0aa..035e1300ca5 100644 --- a/Content.Client/Buckle/BuckleSystem.cs +++ b/Content.Client/Buckle/BuckleSystem.cs @@ -15,7 +15,6 @@ public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnHandleState); SubscribeLocalEvent(OnAppearanceChange); SubscribeLocalEvent(OnStrapMoveEvent); } @@ -57,21 +56,6 @@ private void OnStrapMoveEvent(EntityUid uid, StrapComponent component, ref MoveE } } - private void OnHandleState(Entity ent, ref ComponentHandleState args) - { - if (args.Current is not BuckleState state) - return; - - ent.Comp.DontCollide = state.DontCollide; - ent.Comp.BuckleTime = state.BuckleTime; - var strapUid = EnsureEntity(state.BuckledTo, ent); - - SetBuckledTo(ent, strapUid == null ? null : new (strapUid.Value, null)); - - var (uid, component) = ent; - - } - private void OnAppearanceChange(EntityUid uid, BuckleComponent component, ref AppearanceChangeEvent args) { if (!TryComp(uid, out var rotVisuals)) diff --git a/Content.Client/Cargo/Systems/ClientPriceGunSystem.cs b/Content.Client/Cargo/Systems/ClientPriceGunSystem.cs new file mode 100644 index 00000000000..f1739324783 --- /dev/null +++ b/Content.Client/Cargo/Systems/ClientPriceGunSystem.cs @@ -0,0 +1,21 @@ +using Content.Shared.Timing; +using Content.Shared.Cargo.Systems; + +namespace Content.Client.Cargo.Systems; + +/// +/// This handles... +/// +public sealed class ClientPriceGunSystem : SharedPriceGunSystem +{ + [Dependency] private readonly UseDelaySystem _useDelay = default!; + + protected override bool GetPriceOrBounty(EntityUid priceGunUid, EntityUid target, EntityUid user) + { + if (!TryComp(priceGunUid, out UseDelayComponent? useDelay) || _useDelay.IsDelayed((priceGunUid, useDelay))) + return false; + + // It feels worse if the cooldown is predicted but the popup isn't! So only do the cooldown reset on the server. + return true; + } +} diff --git a/Content.Client/Changelog/ChangelogTab.xaml.cs b/Content.Client/Changelog/ChangelogTab.xaml.cs index b8f98c0d408..00abd642fe8 100644 --- a/Content.Client/Changelog/ChangelogTab.xaml.cs +++ b/Content.Client/Changelog/ChangelogTab.xaml.cs @@ -131,13 +131,13 @@ public void PopulateChangelog(ChangelogManager.Changelog changelog) Margin = new Thickness(6, 0, 0, 0), }; authorLabel.SetMessage( - FormattedMessage.FromMarkup(Loc.GetString("changelog-author-changed", ("author", author)))); + FormattedMessage.FromMarkupOrThrow(Loc.GetString("changelog-author-changed", ("author", author)))); ChangelogBody.AddChild(authorLabel); foreach (var change in groupedEntry.SelectMany(c => c.Changes)) { var text = new RichTextLabel(); - text.SetMessage(FormattedMessage.FromMarkup(change.Message)); + text.SetMessage(FormattedMessage.FromMarkupOrThrow(change.Message)); ChangelogBody.AddChild(new BoxContainer { Orientation = LayoutOrientation.Horizontal, diff --git a/Content.Client/Chat/Managers/ChatManager.cs b/Content.Client/Chat/Managers/ChatManager.cs index f0c73778ec0..275589f98c8 100644 --- a/Content.Client/Chat/Managers/ChatManager.cs +++ b/Content.Client/Chat/Managers/ChatManager.cs @@ -22,6 +22,16 @@ public void Initialize() _sawmill.Level = LogLevel.Info; } + public void SendAdminAlert(string message) + { + // See server-side manager. This just exists for shared code. + } + + public void SendAdminAlert(EntityUid player, string message) + { + // See server-side manager. This just exists for shared code. + } + public void SendMessage(string text, ChatSelectChannel channel) { var str = text.ToString(); diff --git a/Content.Client/Chat/Managers/IChatManager.cs b/Content.Client/Chat/Managers/IChatManager.cs index a21a8194fde..f7317981977 100644 --- a/Content.Client/Chat/Managers/IChatManager.cs +++ b/Content.Client/Chat/Managers/IChatManager.cs @@ -2,10 +2,8 @@ namespace Content.Client.Chat.Managers { - public interface IChatManager + public interface IChatManager : ISharedChatManager { - void Initialize(); - public void SendMessage(string text, ChatSelectChannel channel); /// diff --git a/Content.Client/Chat/UI/SpeechBubble.cs b/Content.Client/Chat/UI/SpeechBubble.cs index adb61d10e62..32e9f4ae9be 100644 --- a/Content.Client/Chat/UI/SpeechBubble.cs +++ b/Content.Client/Chat/UI/SpeechBubble.cs @@ -180,7 +180,7 @@ protected FormattedMessage FormatSpeech(string message, Color? fontColor = null) var msg = new FormattedMessage(); if (fontColor != null) msg.PushColor(fontColor.Value); - msg.AddMarkup(message); + msg.AddMarkupOrThrow(message); return msg; } diff --git a/Content.Client/Chemistry/EntitySystems/ChemistryGuideDataSystem.cs b/Content.Client/Chemistry/EntitySystems/ChemistryGuideDataSystem.cs index a3cedb5f2f3..7c7d824ee98 100644 --- a/Content.Client/Chemistry/EntitySystems/ChemistryGuideDataSystem.cs +++ b/Content.Client/Chemistry/EntitySystems/ChemistryGuideDataSystem.cs @@ -1,5 +1,5 @@ using System.Linq; -using Content.Client.Chemistry.Containers.EntitySystems; +using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Atmos.Prototypes; using Content.Shared.Body.Part; using Content.Shared.Chemistry; @@ -16,7 +16,7 @@ namespace Content.Client.Chemistry.EntitySystems; /// public sealed class ChemistryGuideDataSystem : SharedChemistryGuideDataSystem { - [Dependency] private readonly SolutionContainerSystem _solutionContainer = default!; + [Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!; [ValidatePrototypeId] private const string DefaultMixingCategory = "DummyMix"; diff --git a/Content.Client/Clothing/ClientClothingSystem.cs b/Content.Client/Clothing/ClientClothingSystem.cs index 96bbcc54f2a..3462fc92360 100644 --- a/Content.Client/Clothing/ClientClothingSystem.cs +++ b/Content.Client/Clothing/ClientClothingSystem.cs @@ -50,7 +50,6 @@ public sealed class ClientClothingSystem : ClothingSystem }; [Dependency] private readonly IResourceCache _cache = default!; - [Dependency] private readonly ISerializationManager _serialization = default!; [Dependency] private readonly InventorySystem _inventorySystem = default!; [Dependency] private readonly DisplacementMapSystem _displacement = default!; @@ -321,7 +320,8 @@ private void RenderEquipment(EntityUid equipee, EntityUid equipment, string slot if (layerData.State is not null && inventory.SpeciesId is not null && layerData.State.EndsWith(inventory.SpeciesId)) continue; - _displacement.TryAddDisplacement(displacementData, sprite, index, key, revealedLayers); + if (_displacement.TryAddDisplacement(displacementData, sprite, index, key, revealedLayers)) + index++; } } diff --git a/Content.Client/Commands/ActionsCommands.cs b/Content.Client/Commands/ActionsCommands.cs index dd489fd4d65..738a6544763 100644 --- a/Content.Client/Commands/ActionsCommands.cs +++ b/Content.Client/Commands/ActionsCommands.cs @@ -1,10 +1,9 @@ -using System.IO; using Content.Client.Actions; -using Content.Client.Mapping; +using System.IO; using Content.Shared.Administration; using Robust.Client.UserInterface; -using Robust.Shared.Console; using YamlDotNet.RepresentationModel; +using Robust.Shared.Console; namespace Content.Client.Commands; @@ -84,27 +83,3 @@ private static async void LoadActs() reader.Close(); } } - -[AnyCommand] -public sealed class LoadMappingActionsCommand : LocalizedCommands -{ - [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; - - public const string CommandName = "loadmapacts"; - - public override string Command => CommandName; - - public override string Help => LocalizationManager.GetString($"cmd-{Command}-help", ("command", Command)); - - public override void Execute(IConsoleShell shell, string argStr, string[] args) - { - try - { - _entitySystemManager.GetEntitySystem().LoadMappingActions(); - } - catch - { - shell.WriteError(LocalizationManager.GetString($"cmd-{Command}-error")); - } - } -} diff --git a/Content.Client/Commands/MappingClientSideSetupCommand.cs b/Content.Client/Commands/MappingClientSideSetupCommand.cs index 39268c62847..3255e85e18f 100644 --- a/Content.Client/Commands/MappingClientSideSetupCommand.cs +++ b/Content.Client/Commands/MappingClientSideSetupCommand.cs @@ -1,6 +1,9 @@ +using Content.Client.Actions; +using Content.Client.Mapping; using Content.Client.Markers; using JetBrains.Annotations; using Robust.Client.Graphics; +using Robust.Client.State; using Robust.Shared.Console; namespace Content.Client.Commands; @@ -10,6 +13,7 @@ internal sealed class MappingClientSideSetupCommand : LocalizedCommands { [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; [Dependency] private readonly ILightManager _lightManager = default!; + [Dependency] private readonly IStateManager _stateManager = default!; public override string Command => "mappingclientsidesetup"; @@ -21,8 +25,8 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args) { _entitySystemManager.GetEntitySystem().MarkersVisible = true; _lightManager.Enabled = false; - shell.ExecuteCommand(ShowSubFloorForever.CommandName); - shell.ExecuteCommand(LoadMappingActionsCommand.CommandName); + shell.ExecuteCommand("showsubfloorforever"); + _entitySystemManager.GetEntitySystem().LoadActionAssignments("/mapping_actions.yml", false); } } } diff --git a/Content.Client/Computer/ComputerBoundUserInterface.cs b/Content.Client/Computer/ComputerBoundUserInterface.cs index 11c26b252e9..9f34eeda20f 100644 --- a/Content.Client/Computer/ComputerBoundUserInterface.cs +++ b/Content.Client/Computer/ComputerBoundUserInterface.cs @@ -11,8 +11,6 @@ namespace Content.Client.Computer [Virtual] public class ComputerBoundUserInterface : ComputerBoundUserInterfaceBase where TWindow : BaseWindow, IComputerWindow, new() where TState : BoundUserInterfaceState { - [Dependency] private readonly IDynamicTypeFactory _dynamicTypeFactory = default!; - [ViewVariables] private TWindow? _window; diff --git a/Content.Client/ContextMenu/UI/ContextMenuUIController.cs b/Content.Client/ContextMenu/UI/ContextMenuUIController.cs index 5b156644a73..2d94034bb9c 100644 --- a/Content.Client/ContextMenu/UI/ContextMenuUIController.cs +++ b/Content.Client/ContextMenu/UI/ContextMenuUIController.cs @@ -2,6 +2,7 @@ using System.Threading; using Content.Client.CombatMode; using Content.Client.Gameplay; +using Content.Client.Mapping; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controllers; using Timer = Robust.Shared.Timing.Timer; @@ -16,7 +17,7 @@ namespace Content.Client.ContextMenu.UI /// /// This largely involves setting up timers to open and close sub-menus when hovering over other menu elements. /// - public sealed class ContextMenuUIController : UIController, IOnStateEntered, IOnStateExited, IOnSystemChanged + public sealed class ContextMenuUIController : UIController, IOnStateEntered, IOnStateExited, IOnSystemChanged, IOnStateEntered, IOnStateExited { public static readonly TimeSpan HoverDelay = TimeSpan.FromSeconds(0.2); @@ -42,18 +43,51 @@ public sealed class ContextMenuUIController : UIController, IOnStateEntered? OnSubMenuOpened; public Action? OnContextKeyEvent; + private bool _setup; + public void OnStateEntered(GameplayState state) { + Setup(); + } + + public void OnStateExited(GameplayState state) + { + Shutdown(); + } + + public void OnStateEntered(MappingState state) + { + Setup(); + } + + public void OnStateExited(MappingState state) + { + Shutdown(); + } + + public void Setup() + { + if (_setup) + return; + + _setup = true; + RootMenu = new(this, null); RootMenu.OnPopupHide += Close; Menus.Push(RootMenu); } - public void OnStateExited(GameplayState state) + public void Shutdown() { + if (!_setup) + return; + + _setup = false; + Close(); RootMenu.OnPopupHide -= Close; RootMenu.Dispose(); + RootMenu = default!; } /// diff --git a/Content.Client/Credits/CreditsWindow.xaml.cs b/Content.Client/Credits/CreditsWindow.xaml.cs index d804246687b..a65f1e3a514 100644 --- a/Content.Client/Credits/CreditsWindow.xaml.cs +++ b/Content.Client/Credits/CreditsWindow.xaml.cs @@ -145,7 +145,7 @@ void AddSection(string title, string path, bool markup = false) var text = _resourceManager.ContentFileReadAllText($"/Credits/{path}"); if (markup) { - label.SetMessage(FormattedMessage.FromMarkup(text.Trim())); + label.SetMessage(FormattedMessage.FromMarkupOrThrow(text.Trim())); } else { diff --git a/Content.Client/CriminalRecords/CriminalRecordsConsoleWindow.xaml.cs b/Content.Client/CriminalRecords/CriminalRecordsConsoleWindow.xaml.cs index 21aa54c9622..7cae290fe17 100644 --- a/Content.Client/CriminalRecords/CriminalRecordsConsoleWindow.xaml.cs +++ b/Content.Client/CriminalRecords/CriminalRecordsConsoleWindow.xaml.cs @@ -227,7 +227,7 @@ private void PopulateRecordContainer(GeneralStationRecord stationRecord, Crimina StatusOptionButton.SelectId((int) criminalRecord.Status); if (criminalRecord.Reason is {} reason) { - var message = FormattedMessage.FromMarkup(Loc.GetString("criminal-records-console-wanted-reason")); + var message = FormattedMessage.FromMarkupOrThrow(Loc.GetString("criminal-records-console-wanted-reason")); message.AddText($": {reason}"); WantedReason.SetMessage(message); WantedReason.Visible = true; diff --git a/Content.Client/Decals/Overlays/DecalPlacementOverlay.cs b/Content.Client/Decals/Overlays/DecalPlacementOverlay.cs index 845bd7c03d2..07b6f57bdb9 100644 --- a/Content.Client/Decals/Overlays/DecalPlacementOverlay.cs +++ b/Content.Client/Decals/Overlays/DecalPlacementOverlay.cs @@ -4,6 +4,7 @@ using Robust.Client.Input; using Robust.Shared.Enums; using Robust.Shared.Map; +using Robust.Shared.Prototypes; namespace Content.Client.Decals.Overlays; @@ -16,7 +17,7 @@ public sealed class DecalPlacementOverlay : Overlay private readonly SharedTransformSystem _transform; private readonly SpriteSystem _sprite; - public override OverlaySpace Space => OverlaySpace.WorldSpace; + public override OverlaySpace Space => OverlaySpace.WorldSpaceEntities; public DecalPlacementOverlay(DecalPlacementSystem placement, SharedTransformSystem transform, SpriteSystem sprite) { @@ -24,6 +25,7 @@ public DecalPlacementOverlay(DecalPlacementSystem placement, SharedTransformSyst _placement = placement; _transform = transform; _sprite = sprite; + ZIndex = 1000; } protected override void Draw(in OverlayDrawArgs args) @@ -55,7 +57,7 @@ protected override void Draw(in OverlayDrawArgs args) if (snap) { - localPos = (Vector2) localPos.Floored() + grid.TileSizeHalfVector; + localPos = localPos.Floored() + grid.TileSizeHalfVector; } // Nothing uses snap cardinals so probably don't need preview? diff --git a/Content.Client/DeltaV/Abilities/CrawlUnderObjectsSystem.cs b/Content.Client/DeltaV/Abilities/CrawlUnderObjectsSystem.cs new file mode 100644 index 00000000000..879a5efee55 --- /dev/null +++ b/Content.Client/DeltaV/Abilities/CrawlUnderObjectsSystem.cs @@ -0,0 +1,44 @@ +using Content.Shared.DeltaV.Abilities; +using Content.Shared.Popups; +using Robust.Client.GameObjects; +using DrawDepth = Content.Shared.DrawDepth.DrawDepth; + +namespace Content.Client.DeltaV.Abilities; + +public sealed partial class HideUnderTableAbilitySystem : SharedCrawlUnderObjectsSystem +{ + [Dependency] private readonly AppearanceSystem _appearance = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnAppearanceChange); + } + + private void OnAppearanceChange(EntityUid uid, + CrawlUnderObjectsComponent component, + AppearanceChangeEvent args) + { + if (!TryComp(uid, out var sprite)) + return; + + _appearance.TryGetData(uid, SneakMode.Enabled, out bool enabled); + if (enabled) + { + if (component.OriginalDrawDepth != null) + return; + + component.OriginalDrawDepth = sprite.DrawDepth; + sprite.DrawDepth = (int) DrawDepth.SmallMobs; + } + else + { + if (component.OriginalDrawDepth == null) + return; + + sprite.DrawDepth = (int) component.OriginalDrawDepth; + component.OriginalDrawDepth = null; + } + } +} diff --git a/Content.Client/DeltaV/Addictions/AddictionSystem.cs b/Content.Client/DeltaV/Addictions/AddictionSystem.cs new file mode 100644 index 00000000000..75ac6969a48 --- /dev/null +++ b/Content.Client/DeltaV/Addictions/AddictionSystem.cs @@ -0,0 +1,8 @@ +using Content.Shared.DeltaV.Addictions; + +namespace Content.Client.DeltaV.Addictions; + +public sealed class AddictionSystem : SharedAddictionSystem +{ + protected override void UpdateTime(EntityUid uid) {} +} diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/MailMetricUiFragment.xaml.cs b/Content.Client/DeltaV/CartridgeLoader/Cartridges/MailMetricUiFragment.xaml.cs index d1560b53668..9b2a9ecaa45 100644 --- a/Content.Client/DeltaV/CartridgeLoader/Cartridges/MailMetricUiFragment.xaml.cs +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/MailMetricUiFragment.xaml.cs @@ -1,4 +1,3 @@ -using System.Runtime.CompilerServices; using Content.Shared.CartridgeLoader.Cartridges; using Robust.Client.AutoGenerated; using Robust.Client.UserInterface.Controls; diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/PriceHistoryTable.xaml b/Content.Client/DeltaV/CartridgeLoader/Cartridges/PriceHistoryTable.xaml new file mode 100644 index 00000000000..058bde07e9c --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/PriceHistoryTable.xaml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/PriceHistoryTable.xaml.cs b/Content.Client/DeltaV/CartridgeLoader/Cartridges/PriceHistoryTable.xaml.cs new file mode 100644 index 00000000000..f5798f44c42 --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/PriceHistoryTable.xaml.cs @@ -0,0 +1,75 @@ +using System.Linq; +using Robust.Client.AutoGenerated; +using Robust.Client.Graphics; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client.DeltaV.CartridgeLoader.Cartridges; + +[GenerateTypedNameReferences] +public sealed partial class PriceHistoryTable : BoxContainer +{ + public PriceHistoryTable() + { + RobustXamlLoader.Load(this); + + // Create the stylebox here so we can use the colors from StockTradingUi + var styleBox = new StyleBoxFlat + { + BackgroundColor = StockTradingUiFragment.PriceBackgroundColor, + ContentMarginLeftOverride = 6, + ContentMarginRightOverride = 6, + ContentMarginTopOverride = 4, + ContentMarginBottomOverride = 4, + BorderColor = StockTradingUiFragment.BorderColor, + BorderThickness = new Thickness(1), + }; + + HistoryPanel.PanelOverride = styleBox; + } + + public void Update(List priceHistory) + { + PriceGrid.RemoveAllChildren(); + + // Take last 5 prices + var lastFivePrices = priceHistory.TakeLast(5).ToList(); + + for (var i = 0; i < lastFivePrices.Count; i++) + { + var price = lastFivePrices[i]; + var previousPrice = i > 0 ? lastFivePrices[i - 1] : price; + var priceChange = ((price - previousPrice) / previousPrice) * 100; + + var entryContainer = new BoxContainer + { + Orientation = LayoutOrientation.Vertical, + MinWidth = 80, + HorizontalAlignment = HAlignment.Center, + }; + + var priceLabel = new Label + { + Text = $"${price:F2}", + HorizontalAlignment = HAlignment.Center, + }; + + var changeLabel = new Label + { + Text = $"{(priceChange >= 0 ? "+" : "")}{priceChange:F2}%", + HorizontalAlignment = HAlignment.Center, + StyleClasses = { "LabelSubText" }, + Modulate = priceChange switch + { + > 0 => StockTradingUiFragment.PositiveColor, + < 0 => StockTradingUiFragment.NegativeColor, + _ => StockTradingUiFragment.NeutralColor, + } + }; + + entryContainer.AddChild(priceLabel); + entryContainer.AddChild(changeLabel); + PriceGrid.AddChild(entryContainer); + } + } +} diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/StockTradingUi.cs b/Content.Client/DeltaV/CartridgeLoader/Cartridges/StockTradingUi.cs new file mode 100644 index 00000000000..45704ee2349 --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/StockTradingUi.cs @@ -0,0 +1,45 @@ +using Robust.Client.UserInterface; +using Content.Client.UserInterface.Fragments; +using Content.Shared.CartridgeLoader; +using Content.Shared.CartridgeLoader.Cartridges; + +namespace Content.Client.DeltaV.CartridgeLoader.Cartridges; + +public sealed partial class StockTradingUi : UIFragment +{ + private StockTradingUiFragment? _fragment; + + public override Control GetUIFragmentRoot() + { + return _fragment!; + } + + public override void Setup(BoundUserInterface userInterface, EntityUid? fragmentOwner) + { + _fragment = new StockTradingUiFragment(); + + _fragment.OnBuyButtonPressed += (company, amount) => + { + SendStockTradingUiMessage(StockTradingUiAction.Buy, company, amount, userInterface); + }; + _fragment.OnSellButtonPressed += (company, amount) => + { + SendStockTradingUiMessage(StockTradingUiAction.Sell, company, amount, userInterface); + }; + } + + public override void UpdateState(BoundUserInterfaceState state) + { + if (state is StockTradingUiState cast) + { + _fragment?.UpdateState(cast); + } + } + + private static void SendStockTradingUiMessage(StockTradingUiAction action, int company, float amount, BoundUserInterface userInterface) + { + var newsMessage = new StockTradingUiMessageEvent(action, company, amount); + var message = new CartridgeUiMessage(newsMessage); + userInterface.SendMessage(message); + } +} diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/StockTradingUiFragment.xaml b/Content.Client/DeltaV/CartridgeLoader/Cartridges/StockTradingUiFragment.xaml new file mode 100644 index 00000000000..00b45584cc4 --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/StockTradingUiFragment.xaml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/StockTradingUiFragment.xaml.cs b/Content.Client/DeltaV/CartridgeLoader/Cartridges/StockTradingUiFragment.xaml.cs new file mode 100644 index 00000000000..b44e8f44c70 --- /dev/null +++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/StockTradingUiFragment.xaml.cs @@ -0,0 +1,269 @@ +using System.Linq; +using Content.Client.Administration.UI.CustomControls; +using Content.Shared.CartridgeLoader.Cartridges; +using Robust.Client.AutoGenerated; +using Robust.Client.Graphics; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client.DeltaV.CartridgeLoader.Cartridges; + +[GenerateTypedNameReferences] +public sealed partial class StockTradingUiFragment : BoxContainer +{ + private readonly Dictionary _companyEntries = new(); + + // Event handlers for the parent UI + public event Action? OnBuyButtonPressed; + public event Action? OnSellButtonPressed; + + // Define colors + public static readonly Color PositiveColor = Color.FromHex("#00ff00"); // Green + public static readonly Color NegativeColor = Color.FromHex("#ff0000"); // Red + public static readonly Color NeutralColor = Color.FromHex("#ffffff"); // White + public static readonly Color BackgroundColor = Color.FromHex("#25252a"); // Dark grey + public static readonly Color PriceBackgroundColor = Color.FromHex("#1a1a1a"); // Darker grey + public static readonly Color BorderColor = Color.FromHex("#404040"); // Light grey + + public StockTradingUiFragment() + { + RobustXamlLoader.Load(this); + } + + public void UpdateState(StockTradingUiState state) + { + NoEntries.Visible = state.Entries.Count == 0; + Balance.Text = Loc.GetString("stock-trading-balance", ("balance", state.Balance)); + + // Clear all existing entries + foreach (var entry in _companyEntries.Values) + { + entry.Container.RemoveAllChildren(); + } + _companyEntries.Clear(); + Entries.RemoveAllChildren(); + + // Add new entries + for (var i = 0; i < state.Entries.Count; i++) + { + var company = state.Entries[i]; + var entry = new CompanyEntry(i, company.LocalizedDisplayName, OnBuyButtonPressed, OnSellButtonPressed); + _companyEntries[i] = entry; + Entries.AddChild(entry.Container); + + var ownedStocks = state.OwnedStocks.GetValueOrDefault(i, 0); + entry.Update(company, ownedStocks); + } + } + + private sealed class CompanyEntry + { + public readonly BoxContainer Container; + private readonly Label _nameLabel; + private readonly Label _priceLabel; + private readonly Label _changeLabel; + private readonly Button _sellButton; + private readonly Button _buyButton; + private readonly Label _sharesLabel; + private readonly LineEdit _amountEdit; + private readonly PriceHistoryTable _priceHistory; + + public CompanyEntry(int companyIndex, + string displayName, + Action? onBuyPressed, + Action? onSellPressed) + { + Container = new BoxContainer + { + Orientation = LayoutOrientation.Vertical, + HorizontalExpand = true, + Margin = new Thickness(0, 0, 0, 2), + }; + + // Company info panel + var companyPanel = new PanelContainer(); + + var mainContent = new BoxContainer + { + Orientation = LayoutOrientation.Vertical, + HorizontalExpand = true, + Margin = new Thickness(8), + }; + + // Top row with company name and price info + var topRow = new BoxContainer + { + Orientation = LayoutOrientation.Horizontal, + HorizontalExpand = true, + }; + + _nameLabel = new Label + { + HorizontalExpand = true, + Text = displayName, + }; + + // Create a panel for price and change + var pricePanel = new PanelContainer + { + HorizontalAlignment = HAlignment.Right, + }; + + // Style the price panel + var priceStyleBox = new StyleBoxFlat + { + BackgroundColor = BackgroundColor, + ContentMarginLeftOverride = 8, + ContentMarginRightOverride = 8, + ContentMarginTopOverride = 4, + ContentMarginBottomOverride = 4, + BorderColor = BorderColor, + BorderThickness = new Thickness(1), + }; + + pricePanel.PanelOverride = priceStyleBox; + + // Container for price and change labels + var priceContainer = new BoxContainer + { + Orientation = LayoutOrientation.Horizontal, + }; + + _priceLabel = new Label(); + + _changeLabel = new Label + { + HorizontalAlignment = HAlignment.Right, + Modulate = NeutralColor, + Margin = new Thickness(15, 0, 0, 0), + }; + + priceContainer.AddChild(_priceLabel); + priceContainer.AddChild(_changeLabel); + pricePanel.AddChild(priceContainer); + + topRow.AddChild(_nameLabel); + topRow.AddChild(pricePanel); + + // Add the top row + mainContent.AddChild(topRow); + + // Add the price history table between top and bottom rows + _priceHistory = new PriceHistoryTable(); + mainContent.AddChild(_priceHistory); + + // Trading controls (bottom row) + var bottomRow = new BoxContainer + { + Orientation = LayoutOrientation.Horizontal, + HorizontalExpand = true, + Margin = new Thickness(0, 5, 0, 0), + }; + + _sharesLabel = new Label + { + Text = Loc.GetString("stock-trading-owned-shares"), + MinWidth = 100, + }; + + _amountEdit = new LineEdit + { + PlaceHolder = Loc.GetString("stock-trading-amount-placeholder"), + HorizontalExpand = true, + MinWidth = 80, + }; + + var buttonContainer = new BoxContainer + { + Orientation = LayoutOrientation.Horizontal, + HorizontalAlignment = HAlignment.Right, + MinWidth = 140, + }; + + _buyButton = new Button + { + Text = Loc.GetString("stock-trading-buy-button"), + MinWidth = 65, + Margin = new Thickness(3, 0, 3, 0), + }; + + _sellButton = new Button + { + Text = Loc.GetString("stock-trading-sell-button"), + MinWidth = 65, + }; + + buttonContainer.AddChild(_buyButton); + buttonContainer.AddChild(_sellButton); + + bottomRow.AddChild(_sharesLabel); + bottomRow.AddChild(_amountEdit); + bottomRow.AddChild(buttonContainer); + + // Add the bottom row last + mainContent.AddChild(bottomRow); + + companyPanel.AddChild(mainContent); + Container.AddChild(companyPanel); + + // Add horizontal separator after the panel + var separator = new HSeparator + { + Margin = new Thickness(5, 3, 5, 5), + }; + Container.AddChild(separator); + + // Button click events + _buyButton.OnPressed += _ => + { + if (float.TryParse(_amountEdit.Text, out var amount) && amount > 0) + onBuyPressed?.Invoke(companyIndex, amount); + }; + + _sellButton.OnPressed += _ => + { + if (float.TryParse(_amountEdit.Text, out var amount) && amount > 0) + onSellPressed?.Invoke(companyIndex, amount); + }; + + // There has to be a better way of doing this + _amountEdit.OnTextChanged += args => + { + var newText = string.Concat(args.Text.Where(char.IsDigit)); + if (newText != args.Text) + _amountEdit.Text = newText; + }; + } + + public void Update(StockCompanyStruct company, int ownedStocks) + { + _nameLabel.Text = company.LocalizedDisplayName; + _priceLabel.Text = $"${company.CurrentPrice:F2}"; + _sharesLabel.Text = Loc.GetString("stock-trading-owned-shares", ("shares", ownedStocks)); + + var priceChange = 0f; + if (company.PriceHistory is { Count: > 0 }) + { + var previousPrice = company.PriceHistory[^1]; + priceChange = (company.CurrentPrice - previousPrice) / previousPrice * 100; + } + + _changeLabel.Text = $"{(priceChange >= 0 ? "+" : "")}{priceChange:F2}%"; + + // Update color based on price change + _changeLabel.Modulate = priceChange switch + { + > 0 => PositiveColor, + < 0 => NegativeColor, + _ => NeutralColor, + }; + + // Update the price history table if not null + if (company.PriceHistory != null) + _priceHistory.Update(company.PriceHistory); + + // Disable sell button if no shares owned + _sellButton.Disabled = ownedStocks <= 0; + } + } +} diff --git a/Content.Client/DeltaV/Chapel/SacrificialAltarSystem.cs b/Content.Client/DeltaV/Chapel/SacrificialAltarSystem.cs new file mode 100644 index 00000000000..7b9b3757e32 --- /dev/null +++ b/Content.Client/DeltaV/Chapel/SacrificialAltarSystem.cs @@ -0,0 +1,5 @@ +using Content.Shared.DeltaV.Chapel; + +namespace Content.Client.DeltaV.Chapel; + +public sealed class SacrificialAltarSystem : SharedSacrificialAltarSystem; diff --git a/Content.Client/Nyanotrasen/Mail/MailComponent.cs b/Content.Client/DeltaV/Mail/MailComponent.cs similarity index 53% rename from Content.Client/Nyanotrasen/Mail/MailComponent.cs rename to Content.Client/DeltaV/Mail/MailComponent.cs index 4f9b6e36892..1603cf7d663 100644 --- a/Content.Client/Nyanotrasen/Mail/MailComponent.cs +++ b/Content.Client/DeltaV/Mail/MailComponent.cs @@ -1,8 +1,9 @@ -using Content.Shared.Mail; +using Content.Shared.DeltaV.Mail; -namespace Content.Client.Mail +namespace Content.Client.DeltaV.Mail { [RegisterComponent] public sealed partial class MailComponent : SharedMailComponent - {} + { + } } diff --git a/Content.Client/Nyanotrasen/Mail/MailSystem.cs b/Content.Client/DeltaV/Mail/MailSystem.cs similarity index 93% rename from Content.Client/Nyanotrasen/Mail/MailSystem.cs rename to Content.Client/DeltaV/Mail/MailSystem.cs index 38e575b4150..b215192140f 100644 --- a/Content.Client/Nyanotrasen/Mail/MailSystem.cs +++ b/Content.Client/DeltaV/Mail/MailSystem.cs @@ -1,9 +1,9 @@ -using Content.Shared.Mail; +using Content.Shared.DeltaV.Mail; using Content.Shared.StatusIcon; using Robust.Client.GameObjects; using Robust.Shared.Prototypes; -namespace Content.Client.Mail; +namespace Content.Client.DeltaV.Mail; /// /// Display a cool stamp on the parcel based on the job of the recipient. @@ -27,7 +27,6 @@ public sealed class MailJobVisualizerSystem : VisualizerSystem { [Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - [Dependency] private readonly SpriteSystem _stateManager = default!; [Dependency] private readonly SpriteSystem _spriteSystem = default!; protected override void OnAppearanceChange(EntityUid uid, MailComponent component, ref AppearanceChangeEvent args) diff --git a/Content.Client/DeltaV/Overlays/PainOverlay.cs b/Content.Client/DeltaV/Overlays/PainOverlay.cs new file mode 100644 index 00000000000..58b227ce777 --- /dev/null +++ b/Content.Client/DeltaV/Overlays/PainOverlay.cs @@ -0,0 +1,52 @@ +using System.Numerics; +using Content.Shared.DeltaV.Pain; +using Robust.Client.Graphics; +using Robust.Client.Player; +using Robust.Shared.Enums; +using Robust.Shared.Prototypes; + +namespace Content.Client.DeltaV.Overlays; + +public sealed partial class PainOverlay : Overlay +{ + [Dependency] private readonly IPrototypeManager _prototype = default!; + [Dependency] private readonly IPlayerManager _player = default!; + [Dependency] private readonly IEntityManager _entity = default!; + + public override bool RequestScreenTexture => true; + public override OverlaySpace Space => OverlaySpace.WorldSpace; + private readonly ShaderInstance _painShader; + private readonly ProtoId _shaderProto = "ChromaticAberration"; + + public PainOverlay() + { + IoCManager.InjectDependencies(this); + _painShader = _prototype.Index(_shaderProto).Instance().Duplicate(); + } + + protected override bool BeforeDraw(in OverlayDrawArgs args) + { + if (_player.LocalEntity is not { Valid: true } player + || !_entity.HasComponent(player)) + { + return false; + } + + return base.BeforeDraw(in args); + } + + protected override void Draw(in OverlayDrawArgs args) + { + if (ScreenTexture is null) + return; + + _painShader.SetParameter("SCREEN_TEXTURE", ScreenTexture); + + var worldHandle = args.WorldHandle; + var viewport = args.WorldBounds; + worldHandle.SetTransform(Matrix3x2.Identity); + worldHandle.UseShader(_painShader); + worldHandle.DrawRect(viewport, Color.White); + worldHandle.UseShader(null); + } +} diff --git a/Content.Client/DeltaV/Overlays/PainSystem.cs b/Content.Client/DeltaV/Overlays/PainSystem.cs new file mode 100644 index 00000000000..9ad436027a2 --- /dev/null +++ b/Content.Client/DeltaV/Overlays/PainSystem.cs @@ -0,0 +1,65 @@ +using Content.Shared.DeltaV.Pain; +using Robust.Client.Graphics; +using Robust.Shared.Player; + +namespace Content.Client.DeltaV.Overlays; + +public sealed partial class PainSystem : EntitySystem +{ + [Dependency] private readonly IOverlayManager _overlayMan = default!; + [Dependency] private readonly ISharedPlayerManager _playerMan = default!; + + private PainOverlay _overlay = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnPainInit); + SubscribeLocalEvent(OnPainShutdown); + SubscribeLocalEvent(OnPlayerAttached); + SubscribeLocalEvent(OnPlayerDetached); + + _overlay = new(); + } + + private void OnPainInit(Entity ent, ref ComponentInit args) + { + if (ent.Owner == _playerMan.LocalEntity && !ent.Comp.Suppressed) + _overlayMan.AddOverlay(_overlay); + } + + private void OnPainShutdown(Entity ent, ref ComponentShutdown args) + { + if (ent.Owner == _playerMan.LocalEntity) + _overlayMan.RemoveOverlay(_overlay); + } + + private void OnPlayerAttached(Entity ent, ref LocalPlayerAttachedEvent args) + { + if (!ent.Comp.Suppressed) + _overlayMan.AddOverlay(_overlay); + } + + private void OnPlayerDetached(Entity ent, ref LocalPlayerDetachedEvent args) + { + _overlayMan.RemoveOverlay(_overlay); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + // Handle showing/hiding overlay based on suppression status + if (_playerMan.LocalEntity is not { } player) + return; + + if (!TryComp(player, out var comp)) + return; + + if (comp.Suppressed && _overlayMan.HasOverlay()) + _overlayMan.RemoveOverlay(_overlay); + else if (!comp.Suppressed && !_overlayMan.HasOverlay()) + _overlayMan.AddOverlay(_overlay); + } +} diff --git a/Content.Client/DeltaV/RoundEnd/NoEorgPopup.xaml b/Content.Client/DeltaV/RoundEnd/NoEorgPopup.xaml new file mode 100644 index 00000000000..31c3b74ea0b --- /dev/null +++ b/Content.Client/DeltaV/RoundEnd/NoEorgPopup.xaml @@ -0,0 +1,29 @@ + + +