diff --git a/Content.Client/Access/UI/GroupedAccessLevelChecklist.xaml b/Content.Client/Access/UI/GroupedAccessLevelChecklist.xaml new file mode 100644 index 0000000000..84d581487d --- /dev/null +++ b/Content.Client/Access/UI/GroupedAccessLevelChecklist.xaml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/Content.Client/Access/UI/GroupedAccessLevelChecklist.xaml.cs b/Content.Client/Access/UI/GroupedAccessLevelChecklist.xaml.cs new file mode 100644 index 0000000000..da68653ce5 --- /dev/null +++ b/Content.Client/Access/UI/GroupedAccessLevelChecklist.xaml.cs @@ -0,0 +1,449 @@ +using Content.Client.Stylesheets; +using Content.Client.UserInterface.Controls; +using Content.Shared.Access; +using Robust.Client.AutoGenerated; +using Robust.Client.Graphics; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Prototypes; +using System.Linq; +using System.Numerics; + +namespace Content.Client.Access.UI; + +[GenerateTypedNameReferences] +public sealed partial class GroupedAccessLevelChecklist : BoxContainer +{ + [Dependency] private readonly IPrototypeManager _protoManager = default!; + + private bool _isMonotone; + private string? _labelStyleClass; + + // Access data + private HashSet> _accessGroups = new(); + private HashSet> _accessLevels = new(); + private HashSet> _activeAccessLevels = new(); + + // Button groups + private readonly ButtonGroup _accessGroupsButtons = new(); + + // Temp values + private int _accessGroupTabIndex = 0; + private bool _canInteract = false; + private List _accessLevelsForTab = new(); + private readonly List _accessLevelEntries = new(); + private readonly Dictionary> _groupedAccessLevels = new(); + + // Events + public event Action>, bool>? OnAccessLevelsChangedEvent; + + /// + /// Creates a UI control for changing access levels. + /// Access levels are organized under a list of tabs by their associated access group. + /// + public GroupedAccessLevelChecklist() + { + RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); + } + + private void ArrangeAccessControls() + { + // Create a list of known access groups with which to populate the UI + _groupedAccessLevels.Clear(); + + foreach (var accessGroup in _accessGroups) + { + if (!_protoManager.TryIndex(accessGroup, out var accessGroupProto)) + continue; + + _groupedAccessLevels.Add(accessGroupProto, new()); + } + + // Ensure that the 'general' access group is added to handle + // misc. access levels that aren't associated with any group + if (_protoManager.TryIndex("General", out var generalAccessProto)) + _groupedAccessLevels.TryAdd(generalAccessProto, new()); + + // Assign known access levels with their associated groups + foreach (var accessLevel in _accessLevels) + { + if (!_protoManager.TryIndex(accessLevel, out var accessLevelProto)) + continue; + + var assigned = false; + + foreach (var (accessGroup, accessLevels) in _groupedAccessLevels) + { + if (!accessGroup.Tags.Contains(accessLevelProto.ID)) + continue; + + assigned = true; + _groupedAccessLevels[accessGroup].Add(accessLevelProto); + } + + if (!assigned && generalAccessProto != null) + _groupedAccessLevels[generalAccessProto].Add(accessLevelProto); + } + + // Remove access groups that have no assigned access levels + foreach (var (group, accessLevels) in _groupedAccessLevels) + { + if (accessLevels.Count == 0) + _groupedAccessLevels.Remove(group); + } + } + + private bool TryRebuildAccessGroupControls() + { + AccessGroupList.DisposeAllChildren(); + AccessLevelChecklist.DisposeAllChildren(); + + // No access level prototypes were assigned to any of the access level groups. + // Either the turret controller has no assigned access levels or their names were invalid. + if (_groupedAccessLevels.Count == 0) + return false; + + // Reorder the access groups alphabetically + var orderedAccessGroups = _groupedAccessLevels.Keys.OrderBy(x => x.GetAccessGroupName()).ToList(); + + // Add group access buttons to the UI + foreach (var accessGroup in orderedAccessGroups) + { + var accessGroupButton = CreateAccessGroupButton(); + + // Button styling + if (_groupedAccessLevels.Count > 1) + { + if (AccessGroupList.ChildCount == 0) + accessGroupButton.AddStyleClass(StyleBase.ButtonOpenLeft); + else if (_groupedAccessLevels.Count > 1 && AccessGroupList.ChildCount == (_groupedAccessLevels.Count - 1)) + accessGroupButton.AddStyleClass(StyleBase.ButtonOpenRight); + else + accessGroupButton.AddStyleClass(StyleBase.ButtonOpenBoth); + } + + accessGroupButton.Pressed = _accessGroupTabIndex == orderedAccessGroups.IndexOf(accessGroup); + + // Label text and styling + if (_labelStyleClass != null) + accessGroupButton.Label.SetOnlyStyleClass(_labelStyleClass); + + var accessLevelPrototypes = _groupedAccessLevels[accessGroup]; + var prefix = accessLevelPrototypes.All(x => _activeAccessLevels.Contains(x)) + ? "»" + : accessLevelPrototypes.Any(x => _activeAccessLevels.Contains(x)) + ? "›" + : " "; + + var text = Loc.GetString( + "turret-controls-window-access-group-label", + ("prefix", prefix), + ("label", accessGroup.GetAccessGroupName()) + ); + + accessGroupButton.Text = text; + + // Button events + accessGroupButton.OnPressed += _ => OnAccessGroupChanged(accessGroupButton.GetPositionInParent()); + + AccessGroupList.AddChild(accessGroupButton); + } + + // Adjust the current tab index so it remains in range + if (_accessGroupTabIndex >= _groupedAccessLevels.Count) + _accessGroupTabIndex = _groupedAccessLevels.Count - 1; + + return true; + } + + /// + /// Rebuilds the checkbox list for the access level controls. + /// + public void RebuildAccessLevelsControls() + { + AccessLevelChecklist.DisposeAllChildren(); + _accessLevelEntries.Clear(); + + // No access level prototypes were assigned to any of the access level groups + // Either turret controller has no assigned access levels, or their names were invalid + if (_groupedAccessLevels.Count == 0) + return; + + // Reorder the access groups alphabetically + var orderedAccessGroups = _groupedAccessLevels.Keys.OrderBy(x => x.GetAccessGroupName()).ToList(); + + // Get the access levels associated with the current tab + var selectedAccessGroupTabProto = orderedAccessGroups[_accessGroupTabIndex]; + _accessLevelsForTab = _groupedAccessLevels[selectedAccessGroupTabProto]; + _accessLevelsForTab = _accessLevelsForTab.OrderBy(x => x.GetAccessLevelName()).ToList(); + + // Add an 'all' checkbox as the first child of the list if it has more than one access level + // Toggling this checkbox on will mark all other boxes below it on/off + var allCheckBox = CreateAccessLevelCheckbox(); + allCheckBox.Text = Loc.GetString("turret-controls-window-all-checkbox"); + + if (_labelStyleClass != null) + allCheckBox.Label.SetOnlyStyleClass(_labelStyleClass); + + // Add the 'all' checkbox events + allCheckBox.OnPressed += args => + { + SetCheckBoxPressedState(_accessLevelEntries, allCheckBox.Pressed); + + var accessLevels = new HashSet>(); + + foreach (var accessLevel in _accessLevelsForTab) + { + accessLevels.Add(accessLevel); + } + + OnAccessLevelsChangedEvent?.Invoke(accessLevels, allCheckBox.Pressed); + }; + + AccessLevelChecklist.AddChild(allCheckBox); + + // Hide the 'all' checkbox if the tab has only one access level + var allCheckBoxVisible = _accessLevelsForTab.Count > 1; + + allCheckBox.Visible = allCheckBoxVisible; + allCheckBox.Disabled = !_canInteract; + + // Add any remaining missing access level buttons to the UI + foreach (var accessLevel in _accessLevelsForTab) + { + // Create the entry + var accessLevelEntry = new AccessLevelEntry(_isMonotone); + + accessLevelEntry.AccessLevel = accessLevel; + accessLevelEntry.CheckBox.Text = accessLevel.GetAccessLevelName(); + accessLevelEntry.CheckBox.Pressed = _activeAccessLevels.Contains(accessLevel); + accessLevelEntry.CheckBox.Disabled = !_canInteract; + + if (_labelStyleClass != null) + accessLevelEntry.CheckBox.Label.SetOnlyStyleClass(_labelStyleClass); + + // Set the checkbox linkage lines + var isEndOfList = _accessLevelsForTab.IndexOf(accessLevel) == (_accessLevelsForTab.Count - 1); + + var lines = new List<(Vector2, Vector2)> + { + (new Vector2(0.5f, 0f), new Vector2(0.5f, isEndOfList ? 0.5f : 1f)), + (new Vector2(0.5f, 0.5f), new Vector2(1f, 0.5f)), + }; + + accessLevelEntry.UpdateCheckBoxLink(lines); + accessLevelEntry.CheckBoxLink.Visible = allCheckBoxVisible; + accessLevelEntry.CheckBoxLink.Modulate = !_canInteract ? Color.Gray : Color.White; + + // Add checkbox events + accessLevelEntry.CheckBox.OnPressed += args => + { + // If the checkbox and its siblings are checked, check the 'all' checkbox too + allCheckBox.Pressed = AreAllCheckBoxesPressed(_accessLevelEntries.Select(x => x.CheckBox)); + + OnAccessLevelsChangedEvent?.Invoke([accessLevelEntry.AccessLevel], accessLevelEntry.CheckBox.Pressed); + }; + + AccessLevelChecklist.AddChild(accessLevelEntry); + _accessLevelEntries.Add(accessLevelEntry); + } + + // Press the 'all' checkbox if all others are pressed + allCheckBox.Pressed = AreAllCheckBoxesPressed(_accessLevelEntries.Select(x => x.CheckBox)); + } + + private bool AreAllCheckBoxesPressed(IEnumerable checkBoxes) + { + foreach (var checkBox in checkBoxes) + { + if (!checkBox.Pressed) + return false; + } + + return true; + } + + private void SetCheckBoxPressedState(List accessLevelEntries, bool pressed) + { + foreach (var accessLevelEntry in accessLevelEntries) + { + accessLevelEntry.CheckBox.Pressed = pressed; + } + } + + + /// + /// Provides the UI with a list of access groups using which list of tabs should be populated. + /// + public void SetAccessGroups(HashSet> accessGroups) + { + _accessGroups = accessGroups; + + ArrangeAccessControls(); + + if (TryRebuildAccessGroupControls()) + RebuildAccessLevelsControls(); + } + + /// + /// Provides the UI with a list of access levels with which it can populate the currently selected tab. + /// + public void SetAccessLevels(HashSet> accessLevels) + { + _accessLevels = accessLevels; + + ArrangeAccessControls(); + + if (TryRebuildAccessGroupControls()) + RebuildAccessLevelsControls(); + } + + /// + /// Sets which access level checkboxes should be marked on the UI. + /// + public void SetActiveAccessLevels(HashSet> activeAccessLevels) + { + _activeAccessLevels = activeAccessLevels; + + if (TryRebuildAccessGroupControls()) + RebuildAccessLevelsControls(); + } + + /// + /// Sets whether the local player can interact with the checkboxes. + /// + public void SetLocalPlayerAccessibility(bool canInteract) + { + _canInteract = canInteract; + + if (TryRebuildAccessGroupControls()) + RebuildAccessLevelsControls(); + } + + /// + /// Sets whether the UI should use monotone buttons and checkboxes. + /// + public void SetMonotone(bool monotone) + { + _isMonotone = monotone; + + if (TryRebuildAccessGroupControls()) + RebuildAccessLevelsControls(); + } + + /// + /// Applies the specified style to the labels on the UI buttons and checkboxes. + /// + public void SetLabelStyleClass(string? styleClass) + { + _labelStyleClass = styleClass; + + if (TryRebuildAccessGroupControls()) + RebuildAccessLevelsControls(); + } + + private void OnAccessGroupChanged(int newTabIndex) + { + if (newTabIndex == _accessGroupTabIndex) + return; + + _accessGroupTabIndex = newTabIndex; + + if (TryRebuildAccessGroupControls()) + RebuildAccessLevelsControls(); + } + + private Button CreateAccessGroupButton() + { + var button = _isMonotone ? new MonotoneButton() : new Button(); + + button.ToggleMode = true; + button.Group = _accessGroupsButtons; + button.Label.HorizontalAlignment = HAlignment.Left; + + return button; + } + + private CheckBox CreateAccessLevelCheckbox() + { + var checkbox = _isMonotone ? new MonotoneCheckBox() : new CheckBox(); + + checkbox.Margin = new Thickness(0, 0, 0, 3); + checkbox.ToggleMode = true; + checkbox.ReservesSpace = false; + + return checkbox; + } + + private sealed class AccessLevelEntry : BoxContainer + { + public ProtoId AccessLevel; + public readonly CheckBox CheckBox; + public readonly LineRenderer CheckBoxLink; + + public AccessLevelEntry(bool monotone) + { + HorizontalExpand = true; + + CheckBoxLink = new LineRenderer + { + SetWidth = 22, + VerticalExpand = true, + Margin = new Thickness(0, -1), + ReservesSpace = false, + }; + + AddChild(CheckBoxLink); + + CheckBox = monotone ? new MonotoneCheckBox() : new CheckBox(); + CheckBox.ToggleMode = true; + CheckBox.Margin = new Thickness(0f, 0f, 0f, 3f); + + AddChild(CheckBox); + } + + public void UpdateCheckBoxLink(List<(Vector2, Vector2)> lines) + { + CheckBoxLink.Lines = lines; + } + } + + private sealed class LineRenderer : Control + { + /// + /// List of lines to render (their start and end x-y coordinates). + /// Position (0,0) is the top left corner of the control and + /// position (1,1) is the bottom right corner. + /// + /// + /// The color of the lines is inherited from the control. + /// + public List<(Vector2, Vector2)> Lines; + + public LineRenderer() + { + Lines = new List<(Vector2, Vector2)>(); + } + + public LineRenderer(List<(Vector2, Vector2)> lines) + { + Lines = lines; + } + + protected override void Draw(DrawingHandleScreen handle) + { + foreach (var line in Lines) + { + var start = PixelPosition + + new Vector2(PixelWidth * line.Item1.X, PixelHeight * line.Item1.Y); + + var end = PixelPosition + + new Vector2(PixelWidth * line.Item2.X, PixelHeight * line.Item2.Y); + + handle.DrawLine(start, end, ActualModulateSelf); + } + } + } +} diff --git a/Content.Client/TurretController/DeployableTurretControllerSystem.cs b/Content.Client/TurretController/DeployableTurretControllerSystem.cs new file mode 100644 index 0000000000..c3b305f821 --- /dev/null +++ b/Content.Client/TurretController/DeployableTurretControllerSystem.cs @@ -0,0 +1,9 @@ +using Content.Shared.TurretController; + +namespace Content.Client.TurretController; + +/// +public sealed class DeployableTurretControllerSystem : SharedDeployableTurretControllerSystem +{ + +} diff --git a/Content.Client/TurretController/TurretControllerWindow.xaml b/Content.Client/TurretController/TurretControllerWindow.xaml new file mode 100644 index 0000000000..5f4af68f91 --- /dev/null +++ b/Content.Client/TurretController/TurretControllerWindow.xaml @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Content.Client/TurretController/TurretControllerWindow.xaml.cs b/Content.Client/TurretController/TurretControllerWindow.xaml.cs new file mode 100644 index 0000000000..020c894f54 --- /dev/null +++ b/Content.Client/TurretController/TurretControllerWindow.xaml.cs @@ -0,0 +1,201 @@ +using Content.Client.Stylesheets; +using Content.Client.UserInterface.Controls; +using Content.Shared.Access; +using Content.Shared.Access.Systems; +using Content.Shared.TurretController; +using Content.Shared.Turrets; +using Robust.Client.AutoGenerated; +using Robust.Client.Player; +using Robust.Client.ResourceManagement; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.CustomControls; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Prototypes; +using System.Numerics; + +namespace Content.Client.TurretController; + +[GenerateTypedNameReferences] +public sealed partial class TurretControllerWindow : BaseWindow +{ + [Dependency] private readonly IEntityManager _entManager = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly IResourceCache _cache = default!; + + private readonly AccessReaderSystem _accessReaderSystem; + + private EntityUid? _owner; + + // Button groups + private readonly ButtonGroup _armamentButtons = new(); + + // Events + public event Action>, bool>? OnAccessLevelsChangedEvent; + public event Action? OnArmamentSettingChangedEvent; + + // Colors + private static readonly Dictionary ThemeColors = new() + { + [TurretArmamentSetting.Safe] = Color.FromHex("#33e633"), + [TurretArmamentSetting.Stun] = Color.FromHex("#dfb827"), + [TurretArmamentSetting.Lethal] = Color.FromHex("#da2a2a") + }; + + public TurretControllerWindow() + { + RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); + + _accessReaderSystem = _entManager.System(); + + CloseButton.OnPressed += _ => Close(); + + // Set up armament buttons + SafeButton.OnToggled += args => OnArmamentButtonPressed(SafeButton, TurretArmamentSetting.Safe); + StunButton.OnToggled += args => OnArmamentButtonPressed(StunButton, TurretArmamentSetting.Stun); + LethalButton.OnToggled += args => OnArmamentButtonPressed(LethalButton, TurretArmamentSetting.Lethal); + + SafeButton.Group = _armamentButtons; + StunButton.Group = _armamentButtons; + LethalButton.Group = _armamentButtons; + + SafeButton.Label.AddStyleClass("ConsoleText"); + StunButton.Label.AddStyleClass("ConsoleText"); + LethalButton.Label.AddStyleClass("ConsoleText"); + + // Set up access configuration buttons + AccessConfiguration.SetMonotone(true); + AccessConfiguration.SetLabelStyleClass("ConsoleText"); + AccessConfiguration.OnAccessLevelsChangedEvent += OnAccessLevelsChanged; + + // Override footer font + var smallFont = _cache.NotoStack(size: 8); + Footer.FontOverride = smallFont; + } + + private void OnAccessLevelsChanged(HashSet> accessLevels, bool isPressed) + { + OnAccessLevelsChangedEvent?.Invoke(accessLevels, isPressed); + } + + private void OnArmamentButtonPressed(MonotoneButton pressedButton, TurretArmamentSetting setting) + { + UpdateTheme(setting); + OnArmamentSettingChangedEvent?.Invoke(setting); + } + + private void Initialize() + { + RefreshLinkedTurrets(new()); + + if (_entManager.TryGetComponent(_owner, out var turretController)) + { + AccessConfiguration.SetAccessGroups(turretController.AccessGroups); + AccessConfiguration.SetAccessLevels(turretController.AccessLevels); + UpdateTheme((TurretArmamentSetting)turretController.ArmamentState); + } + + if (_entManager.TryGetComponent(_owner, out var turretTargetSettings)) + { + RefreshAccessControls(turretTargetSettings.ExemptAccessLevels); + } + } + + public void SetOwner(EntityUid owner) + { + _owner = owner; + + Initialize(); + } + + private void UpdateTheme(TurretArmamentSetting setting) + { + var setPressedOn = setting switch + { + TurretArmamentSetting.Safe => SafeButton, + TurretArmamentSetting.Stun => StunButton, + TurretArmamentSetting.Lethal => LethalButton, + }; + setPressedOn.Pressed = true; + + var canInteract = IsLocalPlayerAllowedToInteract(); + + SafeButton.Disabled = !SafeButton.Pressed && !canInteract; + StunButton.Disabled = !StunButton.Pressed && !canInteract; + LethalButton.Disabled = !LethalButton.Pressed && !canInteract; + + ContentsContainer.Modulate = ThemeColors[setting]; + } + + public void UpdateState(DeployableTurretControllerBoundInterfaceState state) + { + if (_entManager.TryGetComponent(_owner, out var turretController)) + UpdateTheme((TurretArmamentSetting)turretController.ArmamentState); + + if (_entManager.TryGetComponent(_owner, out var turretTargetSettings)) + RefreshAccessControls(turretTargetSettings.ExemptAccessLevels); + + RefreshLinkedTurrets(state.TurretStateByAddress); + } + + public void RefreshLinkedTurrets(Dictionary turretStates) + { + var turretCount = turretStates.Count; + var hasTurrets = turretCount > 0; + + NoLinkedTurretsText.Visible = !hasTurrets; + LinkedTurretsContainer.Visible = hasTurrets; + + LinkedTurretsContainer.RemoveAllChildren(); + + foreach (var (address, state) in turretStates) + { + var text = Loc.GetString( + "turret-controls-window-turret-status", + ("device", address), + ("status", Loc.GetString(state)) + ); + + var label = new Label + { + Text = text, + HorizontalAlignment = HAlignment.Left, + Margin = new Thickness(10f, 0f, 10f, 0f), + HorizontalExpand = true, + SetHeight = 20f, + }; + + label.AddStyleClass("ConsoleText"); + + LinkedTurretsContainer.AddChild(label); + } + + TurretStatusHeader.Text = Loc.GetString("turret-controls-window-turret-status-label", ("count", turretCount)); + } + + public void RefreshAccessControls(HashSet> exemptAccessLevels) + { + AccessConfiguration.SetActiveAccessLevels(exemptAccessLevels); + AccessConfiguration.SetLocalPlayerAccessibility(IsLocalPlayerAllowedToInteract()); + } + + protected override DragMode GetDragModeFor(Vector2 relativeMousePos) + { + return DragMode.Move; + } + + private bool IsLocalPlayerAllowedToInteract() + { + if (_owner == null || _playerManager.LocalSession?.AttachedEntity == null) + return false; + + return _accessReaderSystem.IsAllowed(_playerManager.LocalSession.AttachedEntity.Value, _owner.Value); + } + + public enum TurretArmamentSetting + { + Safe = -1, + Stun = 0, + Lethal = 1, + } +} diff --git a/Content.Client/TurretController/TurretControllerWindowBoundUserInterface.cs b/Content.Client/TurretController/TurretControllerWindowBoundUserInterface.cs new file mode 100644 index 0000000000..ab1635b8d8 --- /dev/null +++ b/Content.Client/TurretController/TurretControllerWindowBoundUserInterface.cs @@ -0,0 +1,44 @@ +using Content.Shared.Access; +using Content.Shared.TurretController; +using Robust.Client.UserInterface; +using Robust.Shared.Prototypes; + +namespace Content.Client.TurretController; + +public sealed class TurretControllerWindowBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey) +{ + [ViewVariables] + private TurretControllerWindow? _window; + + protected override void Open() + { + base.Open(); + + _window = this.CreateWindow(); + _window.SetOwner(Owner); + _window.OpenCentered(); + + _window.OnAccessLevelsChangedEvent += OnAccessLevelChanged; + _window.OnArmamentSettingChangedEvent += OnArmamentSettingChanged; + } + + protected override void UpdateState(BoundUserInterfaceState state) + { + base.UpdateState(state); + + if (state is not DeployableTurretControllerBoundInterfaceState { } castState) + return; + + _window?.UpdateState(castState); + } + + private void OnAccessLevelChanged(HashSet> accessLevels, bool enabled) + { + SendPredictedMessage(new DeployableTurretExemptAccessLevelChangedMessage(accessLevels, enabled)); + } + + private void OnArmamentSettingChanged(TurretControllerWindow.TurretArmamentSetting setting) + { + SendPredictedMessage(new DeployableTurretArmamentSettingChangedMessage((int)setting)); + } +} diff --git a/Content.Client/Turrets/DeployableTurretSystem.cs b/Content.Client/Turrets/DeployableTurretSystem.cs index 5e84b1e01a..05cacba6f1 100644 --- a/Content.Client/Turrets/DeployableTurretSystem.cs +++ b/Content.Client/Turrets/DeployableTurretSystem.cs @@ -84,9 +84,6 @@ public sealed partial class DeployableTurretSystem : SharedDeployableTurretSyste if (_animation.HasRunningAnimation(ent, animPlayer, DeployableTurretComponent.AnimationKey)) return; - if (state == ent.Comp.VisualState) - return; - var targetState = state & DeployableTurretState.Deployed; var destinationState = ent.Comp.VisualState & DeployableTurretState.Deployed; diff --git a/Content.Client/UserInterface/Controls/MonotoneButton.cs b/Content.Client/UserInterface/Controls/MonotoneButton.cs index 7271ee7de7..b19a2c640f 100644 --- a/Content.Client/UserInterface/Controls/MonotoneButton.cs +++ b/Content.Client/UserInterface/Controls/MonotoneButton.cs @@ -7,7 +7,7 @@ namespace Content.Client.UserInterface.Controls; /// /// A button intended for use with a monotone color palette /// -public sealed class MonotoneButton : ContainerButton +public sealed class MonotoneButton : Button { /// /// Specifies the color of the label text when the button is pressed. @@ -15,43 +15,9 @@ public sealed class MonotoneButton : ContainerButton [ViewVariables] public Color AltTextColor { set; get; } = new Color(0.2f, 0.2f, 0.2f); - /// - /// The label that holds the button text. - /// - public Label Label { get; } - - /// - /// The text displayed by the button. - /// - [PublicAPI, ViewVariables] - public string? Text { get => Label.Text; set => Label.Text = value; } - - /// - /// How to align the text inside the button. - /// - [PublicAPI, ViewVariables] - public AlignMode TextAlign { get => Label.Align; set => Label.Align = value; } - - /// - /// If true, the button will allow shrinking and clip text - /// to prevent the text from going outside the bounds of the button. - /// If false, the minimum size will always fit the contained text. - /// - [PublicAPI, ViewVariables] - public bool ClipText - { - get => Label.ClipText; - set => Label.ClipText = value; - } - public MonotoneButton() { - Label = new Label - { - StyleClasses = { StyleClassButton } - }; - - AddChild(Label); + RemoveStyleClass("button"); UpdateAppearance(); } diff --git a/Content.Server/TurretController/DeployableTurretControllerSystem.cs b/Content.Server/TurretController/DeployableTurretControllerSystem.cs new file mode 100644 index 0000000000..f0b6881431 --- /dev/null +++ b/Content.Server/TurretController/DeployableTurretControllerSystem.cs @@ -0,0 +1,152 @@ +using Content.Server.DeviceNetwork.Systems; +using Content.Shared.Access; +using Content.Shared.DeviceNetwork; +using Content.Shared.DeviceNetwork.Components; +using Content.Shared.DeviceNetwork.Events; +using Content.Shared.DeviceNetwork.Systems; +using Content.Shared.TurretController; +using Content.Shared.Turrets; +using Robust.Server.GameObjects; +using Robust.Shared.Prototypes; +using System.Linq; + +namespace Content.Server.TurretController; + +/// +public sealed partial class DeployableTurretControllerSystem : SharedDeployableTurretControllerSystem +{ + [Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!; + [Dependency] private readonly DeviceNetworkSystem _deviceNetwork = default!; + + /// Keys for the device network. See for further examples. + public const string CmdSetArmamemtState = "set_armament_state"; + public const string CmdSetAccessExemptions = "set_access_exemption"; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnBUIOpened); + SubscribeLocalEvent(OnDeviceListUpdate); + SubscribeLocalEvent(OnPacketReceived); + } + + private void OnBUIOpened(Entity ent, ref BoundUIOpenedEvent args) + { + UpdateUIState(ent); + } + + private void OnDeviceListUpdate(Entity ent, ref DeviceListUpdateEvent args) + { + if (!TryComp(ent, out var deviceNetwork)) + return; + + // List of new added turrets + var turretsToAdd = args.Devices.Except(args.OldDevices); + + // Request data from newly linked devices + var payload = new NetworkPayload + { + [DeviceNetworkConstants.Command] = DeviceNetworkConstants.CmdUpdatedState, + }; + + foreach (var turretUid in turretsToAdd) + { + if (!HasComp(turretUid)) + continue; + + if (!TryComp(turretUid, out var turretDeviceNetwork)) + continue; + + _deviceNetwork.QueuePacket(ent, turretDeviceNetwork.Address, payload, device: deviceNetwork); + } + + // Remove newly unlinked devices + var turretsToRemove = args.OldDevices.Except(args.Devices); + var refreshUi = false; + + foreach (var turretUid in turretsToRemove) + { + if (!TryComp(turretUid, out var turretDeviceNetwork)) + continue; + + if (ent.Comp.LinkedTurrets.Remove(turretDeviceNetwork.Address)) + refreshUi = true; + } + + if (refreshUi) + UpdateUIState(ent); + } + + private void OnPacketReceived(Entity ent, ref DeviceNetworkPacketEvent args) + { + if (!args.Data.TryGetValue(DeviceNetworkConstants.Command, out string? command)) + return; + + if (!TryComp(ent, out var deviceNetwork) || deviceNetwork.ReceiveFrequency != args.Frequency) + return; + + // If an update was received from a turret, connect to it and update the UI + if (command == DeviceNetworkConstants.CmdUpdatedState && + args.Data.TryGetValue(command, out DeployableTurretState updatedState)) + { + ent.Comp.LinkedTurrets[args.SenderAddress] = updatedState; + UpdateUIState(ent); + } + } + + protected override void ChangeArmamentSetting(Entity ent, int armamentState, EntityUid? user = null) + { + base.ChangeArmamentSetting(ent, armamentState, user); + + if (!TryComp(ent, out var device)) + return; + + // Update linked turrets' armament statuses + var payload = new NetworkPayload + { + [DeviceNetworkConstants.Command] = CmdSetArmamemtState, + [CmdSetArmamemtState] = armamentState, + }; + + _deviceNetwork.QueuePacket(ent, null, payload, device: device); + } + + protected override void ChangeExemptAccessLevels( + Entity ent, + HashSet> exemptions, + bool enabled, + EntityUid? user = null + ) + { + base.ChangeExemptAccessLevels(ent, exemptions, enabled, user); + + if (!TryComp(ent, out var device) || + !TryComp(ent, out var turretTargetingSettings)) + return; + + // Update linked turrets' target selection exemptions + var payload = new NetworkPayload + { + [DeviceNetworkConstants.Command] = CmdSetAccessExemptions, + [CmdSetAccessExemptions] = turretTargetingSettings.ExemptAccessLevels, + }; + + _deviceNetwork.QueuePacket(ent, null, payload, device: device); + } + + private void UpdateUIState(Entity ent) + { + var turretStates = new Dictionary(); + + foreach (var (address, state) in ent.Comp.LinkedTurrets) + { + var stateName = state.ToString().ToLower(); + var stateDesc = Loc.GetString("turret-controls-window-turret-" + stateName); + turretStates.Add(address, stateDesc); + } + + var uiState = new DeployableTurretControllerBoundInterfaceState(turretStates); + _userInterfaceSystem.SetUiState(ent.Owner, DeployableTurretControllerUiKey.Key, uiState); + } +} diff --git a/Content.Server/Turrets/DeployableTurretSystem.cs b/Content.Server/Turrets/DeployableTurretSystem.cs index 72c011bc90..9bb382badf 100644 --- a/Content.Server/Turrets/DeployableTurretSystem.cs +++ b/Content.Server/Turrets/DeployableTurretSystem.cs @@ -1,20 +1,23 @@ using Content.Server.Destructible; -using Content.Server.DeviceNetwork; -using Content.Server.DeviceNetwork.Components; using Content.Server.DeviceNetwork.Systems; using Content.Server.NPC.HTN; using Content.Server.NPC.HTN.PrimitiveTasks.Operators.Combat.Ranged; using Content.Server.Power.Components; using Content.Server.Repairable; +using Content.Server.TurretController; +using Content.Shared.Access; using Content.Shared.Destructible; using Content.Shared.DeviceNetwork; using Content.Shared.DeviceNetwork.Components; using Content.Shared.DeviceNetwork.Events; using Content.Shared.Power; using Content.Shared.Turrets; +using Content.Shared.Weapons.Ranged.Components; using Content.Shared.Weapons.Ranged.Events; +using Content.Shared.Weapons.Ranged.Systems; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; +using Robust.Shared.Prototypes; using Robust.Shared.Timing; namespace Content.Server.Turrets; @@ -25,6 +28,8 @@ public sealed partial class DeployableTurretSystem : SharedDeployableTurretSyste [Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly DeviceNetworkSystem _deviceNetwork = default!; + [Dependency] private readonly BatteryWeaponFireModesSystem _fireModes = default!; + [Dependency] private readonly TurretTargetSettingsSystem _turretTargetingSettings = default!; [Dependency] private readonly IGameTiming _timing = default!; public override void Initialize() @@ -36,6 +41,7 @@ public sealed partial class DeployableTurretSystem : SharedDeployableTurretSyste SubscribeLocalEvent(OnPowerChanged); SubscribeLocalEvent(OnBroken); SubscribeLocalEvent(OnRepaired); + SubscribeLocalEvent(OnPacketReceived); SubscribeLocalEvent(OnBeforeBroadcast); } @@ -68,6 +74,39 @@ public sealed partial class DeployableTurretSystem : SharedDeployableTurretSyste _appearance.SetData(ent, DeployableTurretVisuals.Broken, false, appearance); } + private void OnPacketReceived(Entity ent, ref DeviceNetworkPacketEvent args) + { + if (!args.Data.TryGetValue(DeviceNetworkConstants.Command, out string? command)) + return; + + // Received a command to change armament state + if (command == DeployableTurretControllerSystem.CmdSetArmamemtState && + args.Data.TryGetValue(command, out int? armamentState)) + { + if (TryComp(ent, out var batteryWeaponFireModes)) + _fireModes.TrySetFireMode(ent, batteryWeaponFireModes, armamentState.Value); + + TrySetState(ent, armamentState.Value >= 0); + return; + } + + // Received a command to change access exemptions + if (command == DeployableTurretControllerSystem.CmdSetAccessExemptions && + args.Data.TryGetValue(command, out HashSet>? accessExemptions) && + TryComp(ent, out var turretTargetSettings)) + { + _turretTargetingSettings.SyncAccessLevelExemptions((ent, turretTargetSettings), accessExemptions); + return; + } + + // Received a command to update the device network + if (command == DeviceNetworkConstants.CmdUpdatedState) + { + SendStateUpdateToDeviceNetwork(ent); + return; + } + } + private void OnBeforeBroadcast(Entity ent, ref BeforeBroadcastAttemptEvent args) { if (!TryComp(ent, out var deviceNetwork)) diff --git a/Content.Shared/Access/AccessGroupPrototype.cs b/Content.Shared/Access/AccessGroupPrototype.cs index ca73aa3dd0..78292df44d 100644 --- a/Content.Shared/Access/AccessGroupPrototype.cs +++ b/Content.Shared/Access/AccessGroupPrototype.cs @@ -1,6 +1,5 @@ -using Content.Shared.Access.Components; +using Content.Shared.Access.Components; using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set; namespace Content.Shared.Access; @@ -14,6 +13,23 @@ public sealed partial class AccessGroupPrototype : IPrototype [IdDataField] public string ID { get; private set; } = default!; - [DataField("tags", required: true)] + /// + /// The player-visible name of the access level group + /// + [DataField] + public string? Name { get; set; } + + /// + /// The access levels associated with this group + /// + [DataField(required: true)] public HashSet> Tags = default!; + + public string GetAccessGroupName() + { + if (Name is { } name) + return Loc.GetString(name); + + return ID; + } } diff --git a/Content.Shared/TurretController/DeployableTurretControllerComponent.cs b/Content.Shared/TurretController/DeployableTurretControllerComponent.cs new file mode 100644 index 0000000000..3bada93413 --- /dev/null +++ b/Content.Shared/TurretController/DeployableTurretControllerComponent.cs @@ -0,0 +1,102 @@ +using Content.Shared.Access; +using Content.Shared.Turrets; +using Robust.Shared.Audio; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; + +namespace Content.Shared.TurretController; + +/// +/// Attached to entities that can set data on linked turret-based entities +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[Access(typeof(SharedDeployableTurretControllerSystem))] +public sealed partial class DeployableTurretControllerComponent : Component +{ + /// + /// The states of the turrets linked to this entity, indexed by their device address. + /// This is used to populate the controller UI with the address and state of linked turrets. + /// + [ViewVariables] + public Dictionary LinkedTurrets = new(); + + /// + /// The last armament state index applied to any linked turrets. + /// Values greater than zero have no additional effect if the linked turrets + /// do not have the + /// + /// + /// -1: Inactive, 0: weapon mode A, 1: weapon mode B, etc. + /// + [DataField, AutoNetworkedField] + public int ArmamentState = -1; + + /// + /// Access level prototypes that are known to the entity. + /// Determines what access permissions can be adjusted. + /// It is also used to populate the controller UI. + /// + [DataField] + public HashSet> AccessLevels = new(); + + /// + /// Access group prototypes that are known to the entity. + /// Determines how access permissions are organized on the controller UI. + /// + [DataField] + public HashSet> AccessGroups = new(); + + /// + /// Sound to play when denying access to the device. + /// + [DataField] + public SoundSpecifier AccessDeniedSound = new SoundPathSpecifier("/Audio/Machines/custom_deny.ogg"); +} + +[Serializable, NetSerializable] +public sealed class DeployableTurretControllerBoundInterfaceState : BoundUserInterfaceState +{ + public Dictionary TurretStateByAddress; + + public DeployableTurretControllerBoundInterfaceState(Dictionary turretStateByAddress) + { + TurretStateByAddress = turretStateByAddress; + } +} + +[Serializable, NetSerializable] +public sealed class DeployableTurretArmamentSettingChangedMessage : BoundUserInterfaceMessage +{ + public int ArmamentState; + + public DeployableTurretArmamentSettingChangedMessage(int armamentState) + { + ArmamentState = armamentState; + } +} + +[Serializable, NetSerializable] +public sealed class DeployableTurretExemptAccessLevelChangedMessage : BoundUserInterfaceMessage +{ + public HashSet> AccessLevels; + public bool Enabled; + + public DeployableTurretExemptAccessLevelChangedMessage(HashSet> accessLevels, bool enabled) + { + AccessLevels = accessLevels; + Enabled = enabled; + } +} + +[Serializable, NetSerializable] +public enum TurretControllerVisuals : byte +{ + ControlPanel, +} + +[Serializable, NetSerializable] +public enum DeployableTurretControllerUiKey : byte +{ + Key, +} diff --git a/Content.Shared/TurretController/SharedDeployableTurretControllerSystem.cs b/Content.Shared/TurretController/SharedDeployableTurretControllerSystem.cs new file mode 100644 index 0000000000..314a98071a --- /dev/null +++ b/Content.Shared/TurretController/SharedDeployableTurretControllerSystem.cs @@ -0,0 +1,96 @@ +using Content.Shared.Access; +using Content.Shared.Access.Systems; +using Content.Shared.Popups; +using Content.Shared.Turrets; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Prototypes; + +namespace Content.Shared.TurretController; + +/// +/// Oversees entities that can change the component values of linked deployable turrets, +/// specifically their armament and access level exemptions, via an associated UI +/// +public abstract partial class SharedDeployableTurretControllerSystem : EntitySystem +{ + [Dependency] private readonly AccessReaderSystem _accessreader = default!; + [Dependency] private readonly TurretTargetSettingsSystem _turretTargetingSettings = default!; + [Dependency] private readonly SharedUserInterfaceSystem _userInterfaceSystem = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + + public override void Initialize() + { + base.Initialize(); + + // Handling of client messages + SubscribeLocalEvent(OnArmamentSettingChanged); + SubscribeLocalEvent(OnExemptAccessLevelsChanged); + } + + private void OnArmamentSettingChanged(Entity ent, ref DeployableTurretArmamentSettingChangedMessage args) + { + if (IsUserAllowedAccess(ent, args.Actor)) + ChangeArmamentSetting(ent, args.ArmamentState, args.Actor); + + if (_userInterfaceSystem.TryGetOpenUi(ent.Owner, DeployableTurretControllerUiKey.Key, out var bui)) + bui.Update(); + } + + private void OnExemptAccessLevelsChanged(Entity ent, ref DeployableTurretExemptAccessLevelChangedMessage args) + { + if (IsUserAllowedAccess(ent, args.Actor)) + ChangeExemptAccessLevels(ent, args.AccessLevels, args.Enabled, args.Actor); + + if (_userInterfaceSystem.TryGetOpenUi(ent.Owner, DeployableTurretControllerUiKey.Key, out var bui)) + bui.Update(); + } + + protected virtual void ChangeArmamentSetting(Entity ent, int armamentState, EntityUid? user = null) + { + ent.Comp.ArmamentState = armamentState; + Dirty(ent); + + _appearance.SetData(ent, TurretControllerVisuals.ControlPanel, armamentState); + + // Linked turrets are updated on the server side + } + + protected virtual void ChangeExemptAccessLevels( + Entity ent, + HashSet> exemptions, + bool enabled, + EntityUid? user = null + ) + { + // Update the controller + if (!TryComp(ent, out var targetSettings)) + return; + + var controller = new Entity(ent, targetSettings); + + foreach (var accessLevel in exemptions) + { + if (!ent.Comp.AccessLevels.Contains(accessLevel)) + continue; + + _turretTargetingSettings.SetAccessLevelExemption(controller, accessLevel, enabled); + } + + Dirty(controller); + + // Linked turrets are updated on the server side + } + + public bool IsUserAllowedAccess(Entity ent, EntityUid user) + { + if (_accessreader.IsAllowed(user, ent)) + return true; + + _popup.PopupClient(Loc.GetString("turret-controls-access-denied"), ent, user); + _audio.PlayPredicted(ent.Comp.AccessDeniedSound, ent, user); + + return false; + } +} diff --git a/Content.Shared/Turrets/TurretTargetSettingsSystem.cs b/Content.Shared/Turrets/TurretTargetSettingsSystem.cs index 56f60e0e69..3a8edc9955 100644 --- a/Content.Shared/Turrets/TurretTargetSettingsSystem.cs +++ b/Content.Shared/Turrets/TurretTargetSettingsSystem.cs @@ -23,13 +23,17 @@ public sealed partial class TurretTargetSettingsSystem : EntitySystem /// The entity and its /// The proto ID for the access level /// Set 'true' to add the exemption, or 'false' to remove it + /// Set 'true' to dirty the component [PublicAPI] - public void SetAccessLevelExemption(Entity ent, ProtoId exemption, bool enabled) + public void SetAccessLevelExemption(Entity ent, ProtoId exemption, bool enabled, bool dirty = true) { if (enabled) ent.Comp.ExemptAccessLevels.Add(exemption); else ent.Comp.ExemptAccessLevels.Remove(exemption); + + if (dirty) + Dirty(ent); } /// @@ -42,7 +46,9 @@ public sealed partial class TurretTargetSettingsSystem : EntitySystem public void SetAccessLevelExemptions(Entity ent, ICollection> exemptions, bool enabled) { foreach (var exemption in exemptions) - SetAccessLevelExemption(ent, exemption, enabled); + SetAccessLevelExemption(ent, exemption, enabled, false); + + Dirty(ent); } /// diff --git a/Resources/Locale/en-US/ui/turret-controls.ftl b/Resources/Locale/en-US/ui/turret-controls.ftl new file mode 100644 index 0000000000..549781a866 --- /dev/null +++ b/Resources/Locale/en-US/ui/turret-controls.ftl @@ -0,0 +1,31 @@ +# Headings +turret-controls-window-title = Autonomous Defense Control System +turret-controls-window-turret-status-label = Linked devices [{$count}] +turret-controls-window-armament-controls-label = Armament setting +turret-controls-window-targeting-controls-label = Authorized personnel + +# Status reports +turret-controls-window-no-turrets = +turret-controls-window-turret-status = » {$device} - Status: {$status} +turret-controls-window-turret-disabled = ***OFFLINE*** +turret-controls-window-turret-retracted = INACTIVE +turret-controls-window-turret-retracting = DEACTIVATING +turret-controls-window-turret-deployed = SEARCHING... +turret-controls-window-turret-deploying = ACTIVATING +turret-controls-window-turret-firing = ENGAGING TARGET +turret-controls-window-turret-error = ERROR [404] + +# Buttons +turret-controls-window-safe = Inactive +turret-controls-window-stun = Stun +turret-controls-window-lethal = Lethal +turret-controls-window-ignore = Ignore +turret-controls-window-target = Target +turret-controls-window-access-group-label = {$prefix} {$label} +turret-controls-window-all-checkbox = All + +# Flavor +turret-controls-window-footer = Unauthorized personnel should ensure defenses are inactive before proceeding + +# Warnings +turret-controls-access-denied = Access denied \ No newline at end of file diff --git a/Resources/Prototypes/Access/misc.yml b/Resources/Prototypes/Access/misc.yml index d3f6df775b..5c8d5e3660 100644 --- a/Resources/Prototypes/Access/misc.yml +++ b/Resources/Prototypes/Access/misc.yml @@ -34,3 +34,8 @@ - Atmospherics - GenpopEnter - GenpopLeave + +- type: accessGroup + id: General + tags: + - Maintenance diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Turrets/turrets_energy.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Turrets/turrets_energy.yml index 66860ae98c..a8b3c9a88c 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Turrets/turrets_energy.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Turrets/turrets_energy.yml @@ -1,11 +1,9 @@ - type: entity parent: [BaseWeaponEnergyTurret, ConstructibleMachine] id: WeaponEnergyTurretStation - name: sentry turret + name: security turret description: A high-tech autonomous weapons system designed to keep unauthorized personnel out of sensitive areas. components: - - # Physics - type: Fixtures fixtures: body: @@ -25,8 +23,6 @@ layer: - MachineLayer hard: false - - # Sprites and appearance - type: Sprite sprite: Objects/Weapons/Guns/Turrets/sentry_turret.rsi drawdepth: HighFloorObjects @@ -70,20 +66,14 @@ enum.WiresVisualLayers.MaintenancePanel: True: { visible: false } False: { visible: true } - - # HTN - type: HTN enabled: false - - # Faction / control - type: StationAiWhitelist - type: NpcFactionMember factions: - AllHostile - type: AccessReader access: [["Security"]] - - # Weapon systems - type: ProjectileBatteryAmmoProvider proto: BulletEnergyTurretDisabler fireCost: 100 @@ -98,8 +88,6 @@ - Security - Borg - BasicSilicon - - # Defenses / destruction - type: DeployableTurret retractedDamageModifierSetId: Metallic deployedDamageModifierSetId: FlimsyMetallic @@ -130,8 +118,6 @@ node: machineFrame - !type:DoActsBehavior acts: ["Destruction"] - - # Device network - type: DeviceNetwork deviceNetId: Wired receiveFrequencyId: TurretControl @@ -141,8 +127,6 @@ examinableAddress: true - type: DeviceNetworkRequiresPower - type: WiredNetworkConnection - - # Wires - type: UserInterface interfaces: enum.WiresUiKey.Key: @@ -156,8 +140,6 @@ locked: true unlockOnClick: false - type: LockedWiresPanel - - # General properties - type: Machine board: WeaponEnergyTurretStationMachineCircuitboard - type: UseDelay @@ -170,7 +152,7 @@ description: A high-tech autonomous weapons system under the direct control of a local artifical intelligence. components: - type: AccessReader - access: [["StationAi"]] + access: [["StationAi"], ["ResearchDirector"]] - type: TurretTargetSettings exemptAccessLevels: - Borg @@ -180,3 +162,4 @@ - type: DeviceNetwork receiveFrequencyId: TurretControlAI transmitFrequencyId: TurretAI + diff --git a/Resources/Prototypes/Entities/Structures/Wallmounts/turret_controls.yml b/Resources/Prototypes/Entities/Structures/Wallmounts/turret_controls.yml new file mode 100644 index 0000000000..95a3e74b1f --- /dev/null +++ b/Resources/Prototypes/Entities/Structures/Wallmounts/turret_controls.yml @@ -0,0 +1,198 @@ +- type: entity + id: WeaponEnergyTurretControlPanelFrame + name: sentry turret control panel assembly + description: An incomplete wall-mounted assembly for a sentry turret control panel. + categories: [ HideSpawnMenu ] + components: + - type: Sprite + noRot: false + drawdepth: SmallObjects + sprite: Structures/Wallmounts/turret_controls.rsi + layers: + - state: base + - type: Damageable + damageContainer: StructuralInorganic + damageModifierSet: StructuralMetallic + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 200 + behaviors: + - !type:PlaySoundBehavior + sound: + collection: MetalGlassBreak + params: + volume: -4 + #- !type:ChangeConstructionNodeBehavior - To be added in a later PR + # node: machineFrame + - !type:DoActsBehavior + acts: [ "Destruction" ] + - type: Transform + anchored: true + - type: WallMount + - type: Clickable + - type: InteractionOutline + - type: ContainerContainer + containers: + board: !type:Container + #- type: Construction - To be added in a later PR + # graph: WeaponEnergyTurretControlPanel + # node: frame + placement: + mode: SnapgridCenter + snap: + - Wallmount + +- type: entity + parent: WeaponEnergyTurretControlPanelFrame + id: WeaponEnergyTurretStationControlPanel + name: security turret control panel + description: A wall-mounted interface for remotely configuring the operational parameters of linked security turrets. + components: + - type: Appearance + - type: Sprite + noRot: false + drawdepth: SmallObjects + sprite: Structures/Wallmounts/turret_controls.rsi + layers: + - state: base + - state: safe + map: ["enum.PowerDeviceVisualLayers.Powered"] + shader: unshaded + - state: wires + map: ["enum.WiresVisualLayers.MaintenancePanel"] + visible: false + - type: GenericVisualizer + visuals: + enum.WiresVisualLayers.MaintenancePanel: + enum.WiresVisualLayers.MaintenancePanel: + True: { visible: true } + False: { visible: false } + enum.PowerDeviceVisuals.Powered: + enum.PowerDeviceVisualLayers.Powered: + True: { visible: true } + False: { visible: false } + enum.TurretControllerVisuals.ControlPanel: + enum.PowerDeviceVisualLayers.Powered: + -1: { state: safe } + 0: { state: stun } + 1: { state: lethal } + - type: StationAiWhitelist + - type: AccessReader + access: [["Security"]] + - type: TurretTargetSettings + exemptAccessLevels: + - Security + - Borg + - BasicSilicon + - type: DeployableTurretController + accessGroups: + - Cargo + - Command + - Engineering + - General + - Medical + - Research + - Security + - Service + - Silicon + accessLevels: + - Armory + - Atmospherics + - Bar + - BasicSilicon + - Borg + - Brig + - Detective + - Captain + - Cargo + - Chapel + - Chemistry + - ChiefEngineer + - ChiefMedicalOfficer + - Command + - Cryogenics + - Engineering + - External + - HeadOfPersonnel + - HeadOfSecurity + - Hydroponics + - Janitor + - Kitchen + - Lawyer + - Maintenance + - Medical + - Quartermaster + - Research + - ResearchDirector + - Salvage + - Security + - Service + - Theatre + - type: DeviceList + isAllowList: true + - type: DeviceNetwork + deviceNetId: Wired + receiveFrequencyId: Turret + transmitFrequencyId: TurretControl + sendBroadcastAttemptEvent: true + prefix: device-address-prefix-console + - type: DeviceNetworkRequiresPower + - type: WiredNetworkConnection + - type: ActivatableUI + key: enum.DeployableTurretControllerUiKey.Key + - type: ActivatableUIRequiresPower + - type: UserInterface + interfaces: + enum.DeployableTurretControllerUiKey.Key: + type: TurretControllerWindowBoundUserInterface + enum.WiresUiKey.Key: + type: WiresBoundUserInterface + - type: WiresPanel + - type: WiresVisuals + - type: Wires + boardName: wires-board-name-turret-controls + layoutId: TurretControls + - type: Lock + locked: true + unlockOnClick: false + - type: LockedWiresPanel + - type: ApcPowerReceiver + - type: ExtensionCableReceiver + - type: Electrified + enabled: false + usesApcPower: true + #- type: ContainerFill - Will be added in a later PR + # containers: + # board: + # - WeaponEnergyTurretStationControlPanelElectronics + #- type: Construction - Will be added in a later PR + # graph: WeaponEnergyTurretControlPanel + # node: finish + +- type: entity + parent: WeaponEnergyTurretStationControlPanel + id: WeaponEnergyTurretAIControlPanel + name: AI sentry turret control panel + description: A wall-mounted interface that allows a local artifical intelligence to adjust the operational parameters of linked sentry turrets. + components: + - type: AccessReader + access: [["StationAi"], ["ResearchDirector"]] + #- type: ContainerFill - Will be added in a later PR + # containers: + # board: + # - WeaponEnergyTurretAIControlPanelElectronics + - type: DeviceNetwork + receiveFrequencyId: TurretAI + transmitFrequencyId: TurretControlAI + - type: TurretTargetSettings + exemptAccessLevels: + - BasicSilicon + - Borg + - type: DeployableTurretController + accessGroups: + - Silicon + accessLevels: + - BasicSilicon + - Borg