From: chromiumboy <50505512+chromiumboy@users.noreply.github.com> Date: Sun, 31 Mar 2024 04:29:47 +0000 (-0500) Subject: Improved RCDs (#22799) X-Git-Url: https://git.smokeofanarchy.ru/gitweb.cgi?a=commitdiff_plain;h=02273ca0e7a4091bac88e585ecca5253dd0e7fd7;p=space-station-14.git Improved RCDs (#22799) * Initial radial menu prototyping for the RCD * Radial UI buttons can send messages to the server * Beginning to update RCDSystem * RCD building system in progress * Further updates * Added extra effects, RCDSystem now reads RCD prototype data * Replacing tiles is instant, multiple constructions are allowed, deconstruction is broken * Added extra functionality to RadialContainers plus documentation * Fixed localization of RCD UI strings * Menu opens near cursor, added basic RCD * Avoiding merge conflict * Implemented atomized construction / deconstruction rules * Increased RCD ammo base charges * Moved input context definition to content * Removed obsoleted code * Updates to system * Switch machine and computer frames for electrical cabling * Added construction ghosts * Fixed issue with keybind detection code * Fixed RCD construction ghost mispredications * Code clean up * Updated deconstruction effects * RCDs effects don't rotate * Code clean up * Balancing for ammo counts * Code clean up * Added missing localized strings * More clean up * Made directional window handling more robust * Added documentation to radial menus and made them no longer dependent on Content * Made radial containers more robust * Further robustness to the radial menu * The RCD submenu buttons are only shown when the destination layer has at least one children * Expanded upon deconstructing plus construction balance * Fixed line endings * Updated list of RCD deconstructable entities. Now needs a component to deconstruct instead of a tag * Bug fixes * Revert unnecessary change * Updated RCD strings * Fixed bug * More fixes * Deconstructed tiles/subflooring convert to lattice instead * Fixed failed tests (Linux doesn't like invalid spritespecifer paths) * Fixing merge conflict * Updated airlock assembly * Fixing merge conflict * Fixing merge conflict * More fixing... * Removed erroneous project file change * Fixed string handling issue * Trying to fix merge conflict * Still fixing merge conflicts * Balancing * Hidden RCD construction ghosts when in 'build' mode * Fixing merge conflict * Implemented requested changes (Part 1) * Added more requested changes * Fix for failed test. Removed sussy null suppression * Made requested changes - custom construction ghost system was replaced * Fixing merge conflict * Fixed merge conflict * Fixed bug in RCD construction ghost validation * Fixing merge conflict * Merge conflict fixed * Made required update * Removed lingering RCD deconstruct tag * Fixing merge conflict * Merge conflict fixed * Made requested changes * Bug fixes and balancing * Made string names more consistent * Can no longer stack catwalks --- diff --git a/Content.Client/Input/ContentContexts.cs b/Content.Client/Input/ContentContexts.cs index 589de6d6a7..2e888b3df9 100644 --- a/Content.Client/Input/ContentContexts.cs +++ b/Content.Client/Input/ContentContexts.cs @@ -45,6 +45,9 @@ namespace Content.Client.Input // Not in engine because the engine doesn't understand what a flipped object is common.AddFunction(ContentKeyFunctions.EditorFlipObject); + // Not in engine so that the RCD can rotate objects + common.AddFunction(EngineKeyFunctions.EditorRotateObject); + var human = contexts.GetContext("human"); human.AddFunction(EngineKeyFunctions.MoveUp); human.AddFunction(EngineKeyFunctions.MoveDown); diff --git a/Content.Client/RCD/AlignRCDConstruction.cs b/Content.Client/RCD/AlignRCDConstruction.cs new file mode 100644 index 0000000000..da7b22c91a --- /dev/null +++ b/Content.Client/RCD/AlignRCDConstruction.cs @@ -0,0 +1,122 @@ +using System.Numerics; +using Content.Client.Gameplay; +using Content.Shared.Hands.Components; +using Content.Shared.Interaction; +using Content.Shared.RCD.Components; +using Content.Shared.RCD.Systems; +using Robust.Client.Placement; +using Robust.Client.Player; +using Robust.Client.State; +using Robust.Shared.Map; +using Robust.Shared.Map.Components; + +namespace Content.Client.RCD; + +public sealed class AlignRCDConstruction : PlacementMode +{ + [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] private readonly IMapManager _mapManager = default!; + [Dependency] private readonly SharedMapSystem _mapSystem = default!; + [Dependency] private readonly RCDSystem _rcdSystem = default!; + [Dependency] private readonly SharedTransformSystem _transformSystem = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly IStateManager _stateManager = default!; + + private const float SearchBoxSize = 2f; + private const float PlaceColorBaseAlpha = 0.5f; + + private EntityCoordinates _unalignedMouseCoords = default; + + /// + /// This placement mode is not on the engine because it is content specific (i.e., for the RCD) + /// + public AlignRCDConstruction(PlacementManager pMan) : base(pMan) + { + var dependencies = IoCManager.Instance!; + _entityManager = dependencies.Resolve(); + _mapManager = dependencies.Resolve(); + _playerManager = dependencies.Resolve(); + _stateManager = dependencies.Resolve(); + + _mapSystem = _entityManager.System(); + _rcdSystem = _entityManager.System(); + _transformSystem = _entityManager.System(); + + ValidPlaceColor = ValidPlaceColor.WithAlpha(PlaceColorBaseAlpha); + } + + public override void AlignPlacementMode(ScreenCoordinates mouseScreen) + { + _unalignedMouseCoords = ScreenToCursorGrid(mouseScreen); + MouseCoords = _unalignedMouseCoords.AlignWithClosestGridTile(SearchBoxSize, _entityManager, _mapManager); + + var gridId = MouseCoords.GetGridUid(_entityManager); + + if (!_entityManager.TryGetComponent(gridId, out var mapGrid)) + return; + + CurrentTile = _mapSystem.GetTileRef(gridId.Value, mapGrid, MouseCoords); + + float tileSize = mapGrid.TileSize; + GridDistancing = tileSize; + + if (pManager.CurrentPermission!.IsTile) + { + MouseCoords = new EntityCoordinates(MouseCoords.EntityId, new Vector2(CurrentTile.X + tileSize / 2, + CurrentTile.Y + tileSize / 2)); + } + else + { + MouseCoords = new EntityCoordinates(MouseCoords.EntityId, new Vector2(CurrentTile.X + tileSize / 2 + pManager.PlacementOffset.X, + CurrentTile.Y + tileSize / 2 + pManager.PlacementOffset.Y)); + } + } + + public override bool IsValidPosition(EntityCoordinates position) + { + var player = _playerManager.LocalSession?.AttachedEntity; + + // If the destination is out of interaction range, set the placer alpha to zero + if (!_entityManager.TryGetComponent(player, out var xform)) + return false; + + if (!xform.Coordinates.InRange(_entityManager, _transformSystem, position, SharedInteractionSystem.InteractionRange)) + { + InvalidPlaceColor = InvalidPlaceColor.WithAlpha(0); + return false; + } + + // Otherwise restore the alpha value + else + { + InvalidPlaceColor = InvalidPlaceColor.WithAlpha(PlaceColorBaseAlpha); + } + + // Determine if player is carrying an RCD in their active hand + if (!_entityManager.TryGetComponent(player, out var hands)) + return false; + + var heldEntity = hands.ActiveHand?.HeldEntity; + + if (!_entityManager.TryGetComponent(heldEntity, out var rcd)) + return false; + + // Retrieve the map grid data for the position + if (!_rcdSystem.TryGetMapGridData(position, out var mapGridData)) + return false; + + // Determine if the user is hovering over a target + var currentState = _stateManager.CurrentState; + + if (currentState is not GameplayStateBase screen) + return false; + + var target = screen.GetClickedEntity(_unalignedMouseCoords.ToMap(_entityManager, _transformSystem)); + + // Determine if the RCD operation is valid or not + if (!_rcdSystem.IsRCDOperationStillValid(heldEntity.Value, rcd, mapGridData.Value, target, player.Value, false)) + return false; + + return true; + } +} diff --git a/Content.Client/RCD/RCDConstructionGhostSystem.cs b/Content.Client/RCD/RCDConstructionGhostSystem.cs new file mode 100644 index 0000000000..792916b892 --- /dev/null +++ b/Content.Client/RCD/RCDConstructionGhostSystem.cs @@ -0,0 +1,78 @@ +using Content.Shared.Hands.Components; +using Content.Shared.Interaction; +using Content.Shared.RCD; +using Content.Shared.RCD.Components; +using Content.Shared.RCD.Systems; +using Robust.Client.Placement; +using Robust.Client.Player; +using Robust.Shared.Enums; + +namespace Content.Client.RCD; + +public sealed class RCDConstructionGhostSystem : EntitySystem +{ + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly RCDSystem _rcdSystem = default!; + [Dependency] private readonly IPlacementManager _placementManager = default!; + + private string _placementMode = typeof(AlignRCDConstruction).Name; + private Direction _placementDirection = default; + + public override void Update(float frameTime) + { + base.Update(frameTime); + + // Get current placer data + var placerEntity = _placementManager.CurrentPermission?.MobUid; + var placerProto = _placementManager.CurrentPermission?.EntityType; + var placerIsRCD = HasComp(placerEntity); + + // Exit if erasing or the current placer is not an RCD (build mode is active) + if (_placementManager.Eraser || (placerEntity != null && !placerIsRCD)) + return; + + // Determine if player is carrying an RCD in their active hand + var player = _playerManager.LocalSession?.AttachedEntity; + + if (!TryComp(player, out var hands)) + return; + + var heldEntity = hands.ActiveHand?.HeldEntity; + + if (!TryComp(heldEntity, out var rcd)) + { + // If the player was holding an RCD, but is no longer, cancel placement + if (placerIsRCD) + _placementManager.Clear(); + + return; + } + + // Update the direction the RCD prototype based on the placer direction + if (_placementDirection != _placementManager.Direction) + { + _placementDirection = _placementManager.Direction; + RaiseNetworkEvent(new RCDConstructionGhostRotationEvent(GetNetEntity(heldEntity.Value), _placementDirection)); + } + + // If the placer has not changed, exit + _rcdSystem.UpdateCachedPrototype(heldEntity.Value, rcd); + + if (heldEntity == placerEntity && rcd.CachedPrototype.Prototype == placerProto) + return; + + // Create a new placer + var newObjInfo = new PlacementInformation + { + MobUid = heldEntity.Value, + PlacementOption = _placementMode, + EntityType = rcd.CachedPrototype.Prototype, + Range = (int) Math.Ceiling(SharedInteractionSystem.InteractionRange), + IsTile = (rcd.CachedPrototype.Mode == RcdMode.ConstructTile), + UseEditorContext = false, + }; + + _placementManager.Clear(); + _placementManager.BeginPlacing(newObjInfo); + } +} diff --git a/Content.Client/RCD/RCDMenu.xaml b/Content.Client/RCD/RCDMenu.xaml new file mode 100644 index 0000000000..b3d5367a5f --- /dev/null +++ b/Content.Client/RCD/RCDMenu.xaml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Content.Client/RCD/RCDMenu.xaml.cs b/Content.Client/RCD/RCDMenu.xaml.cs new file mode 100644 index 0000000000..8679e789dc --- /dev/null +++ b/Content.Client/RCD/RCDMenu.xaml.cs @@ -0,0 +1,137 @@ +using Content.Client.UserInterface.Controls; +using Content.Shared.RCD; +using Content.Shared.RCD.Components; +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.Prototypes; +using System.Numerics; + +namespace Content.Client.RCD; + +[GenerateTypedNameReferences] +public sealed partial class RCDMenu : RadialMenu +{ + [Dependency] private readonly EntityManager _entManager = default!; + [Dependency] private readonly IPrototypeManager _protoManager = default!; + + private readonly SpriteSystem _spriteSystem; + + public event Action>? SendRCDSystemMessageAction; + + public RCDMenu(EntityUid owner, RCDMenuBoundUserInterface bui) + { + IoCManager.InjectDependencies(this); + RobustXamlLoader.Load(this); + + _spriteSystem = _entManager.System(); + + // Find the main radial container + var main = FindControl("Main"); + + if (main == null) + return; + + // Populate secondary radial containers + if (!_entManager.TryGetComponent(owner, out var rcd)) + return; + + foreach (var protoId in rcd.AvailablePrototypes) + { + if (!_protoManager.TryIndex(protoId, out var proto)) + continue; + + if (proto.Mode == RcdMode.Invalid) + continue; + + var parent = FindControl(proto.Category); + + if (parent == null) + continue; + + var name = Loc.GetString(proto.SetName); + name = char.ToUpper(name[0]) + name.Remove(0, 1); + + var button = new RCDMenuButton() + { + StyleClasses = { "RadialMenuButton" }, + SetSize = new Vector2(64f, 64f), + ToolTip = name, + ProtoId = protoId, + }; + + if (proto.Sprite != null) + { + var tex = new TextureRect() + { + VerticalAlignment = VAlignment.Center, + HorizontalAlignment = HAlignment.Center, + Texture = _spriteSystem.Frame0(proto.Sprite), + TextureScale = new Vector2(2f, 2f), + }; + + button.AddChild(tex); + } + + parent.AddChild(button); + + // Ensure that the button that transitions the menu to the associated category layer + // is visible in the main radial container (as these all start with Visible = false) + foreach (var child in main.Children) + { + var castChild = child as RadialMenuTextureButton; + + if (castChild is not RadialMenuTextureButton) + continue; + + if (castChild.TargetLayer == proto.Category) + { + castChild.Visible = true; + break; + } + } + } + + // Set up menu actions + foreach (var child in Children) + AddRCDMenuButtonOnClickActions(child); + + OnChildAdded += AddRCDMenuButtonOnClickActions; + + SendRCDSystemMessageAction += bui.SendRCDSystemMessage; + } + + private void AddRCDMenuButtonOnClickActions(Control control) + { + var radialContainer = control as RadialContainer; + + if (radialContainer == null) + return; + + foreach (var child in radialContainer.Children) + { + var castChild = child as RCDMenuButton; + + if (castChild == null) + continue; + + castChild.OnButtonUp += _ => + { + SendRCDSystemMessageAction?.Invoke(castChild.ProtoId); + Close(); + }; + } + } +} + +public sealed class RCDMenuButton : RadialMenuTextureButton +{ + public ProtoId ProtoId { get; set; } + + public RCDMenuButton() + { + + } +} diff --git a/Content.Client/RCD/RCDMenuBoundUserInterface.cs b/Content.Client/RCD/RCDMenuBoundUserInterface.cs new file mode 100644 index 0000000000..a37dbcecf8 --- /dev/null +++ b/Content.Client/RCD/RCDMenuBoundUserInterface.cs @@ -0,0 +1,49 @@ +using Content.Shared.RCD; +using Content.Shared.RCD.Components; +using JetBrains.Annotations; +using Robust.Client.Graphics; +using Robust.Client.Input; +using Robust.Shared.Prototypes; + +namespace Content.Client.RCD; + +[UsedImplicitly] +public sealed class RCDMenuBoundUserInterface : BoundUserInterface +{ + [Dependency] private readonly IClyde _displayManager = default!; + [Dependency] private readonly IInputManager _inputManager = default!; + + private RCDMenu? _menu; + + public RCDMenuBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) + { + IoCManager.InjectDependencies(this); + } + + protected override void Open() + { + base.Open(); + + _menu = new(Owner, this); + _menu.OnClose += Close; + + // Open the menu, centered on the mouse + var vpSize = _displayManager.ScreenSize; + _menu.OpenCenteredAt(_inputManager.MouseScreenPosition.Position / vpSize); + } + + public void SendRCDSystemMessage(ProtoId protoId) + { + // A predicted message cannot be used here as the RCD UI is closed immediately + // after this message is sent, which will stop the server from receiving it + SendMessage(new RCDSystemMessage(protoId)); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + if (!disposing) return; + + _menu?.Dispose(); + } +} diff --git a/Content.Client/Stylesheets/StyleNano.cs b/Content.Client/Stylesheets/StyleNano.cs index 426af1616e..2c7a1873a3 100644 --- a/Content.Client/Stylesheets/StyleNano.cs +++ b/Content.Client/Stylesheets/StyleNano.cs @@ -290,7 +290,7 @@ namespace Content.Client.Stylesheets var buttonTex = resCache.GetTexture("/Textures/Interface/Nano/button.svg.96dpi.png"); var topButtonBase = new StyleBoxTexture { - Texture = buttonTex, + Texture = buttonTex, }; topButtonBase.SetPatchMargin(StyleBox.Margin.All, 10); topButtonBase.SetPadding(StyleBox.Margin.All, 0); @@ -298,19 +298,19 @@ namespace Content.Client.Stylesheets var topButtonOpenRight = new StyleBoxTexture(topButtonBase) { - Texture = new AtlasTexture(buttonTex, UIBox2.FromDimensions(new Vector2(0, 0), new Vector2(14, 24))), + Texture = new AtlasTexture(buttonTex, UIBox2.FromDimensions(new Vector2(0, 0), new Vector2(14, 24))), }; topButtonOpenRight.SetPatchMargin(StyleBox.Margin.Right, 0); var topButtonOpenLeft = new StyleBoxTexture(topButtonBase) { - Texture = new AtlasTexture(buttonTex, UIBox2.FromDimensions(new Vector2(10, 0), new Vector2(14, 24))), + Texture = new AtlasTexture(buttonTex, UIBox2.FromDimensions(new Vector2(10, 0), new Vector2(14, 24))), }; topButtonOpenLeft.SetPatchMargin(StyleBox.Margin.Left, 0); var topButtonSquare = new StyleBoxTexture(topButtonBase) { - Texture = new AtlasTexture(buttonTex, UIBox2.FromDimensions(new Vector2(10, 0), new Vector2(3, 24))), + Texture = new AtlasTexture(buttonTex, UIBox2.FromDimensions(new Vector2(10, 0), new Vector2(3, 24))), }; topButtonSquare.SetPatchMargin(StyleBox.Margin.Horizontal, 0); @@ -368,9 +368,9 @@ namespace Content.Client.Stylesheets }; tabContainerPanel.SetPatchMargin(StyleBox.Margin.All, 2); - var tabContainerBoxActive = new StyleBoxFlat {BackgroundColor = new Color(64, 64, 64)}; + var tabContainerBoxActive = new StyleBoxFlat { BackgroundColor = new Color(64, 64, 64) }; tabContainerBoxActive.SetContentMarginOverride(StyleBox.Margin.Horizontal, 5); - var tabContainerBoxInactive = new StyleBoxFlat {BackgroundColor = new Color(32, 32, 32)}; + var tabContainerBoxInactive = new StyleBoxFlat { BackgroundColor = new Color(32, 32, 32) }; tabContainerBoxInactive.SetContentMarginOverride(StyleBox.Margin.Horizontal, 5); var progressBarBackground = new StyleBoxFlat @@ -409,21 +409,21 @@ namespace Content.Client.Stylesheets // Placeholder var placeholderTexture = resCache.GetTexture("/Textures/Interface/Nano/placeholder.png"); - var placeholder = new StyleBoxTexture {Texture = placeholderTexture}; + var placeholder = new StyleBoxTexture { Texture = placeholderTexture }; placeholder.SetPatchMargin(StyleBox.Margin.All, 19); placeholder.SetExpandMargin(StyleBox.Margin.All, -5); placeholder.Mode = StyleBoxTexture.StretchMode.Tile; - var itemListBackgroundSelected = new StyleBoxFlat {BackgroundColor = new Color(75, 75, 86)}; + var itemListBackgroundSelected = new StyleBoxFlat { BackgroundColor = new Color(75, 75, 86) }; itemListBackgroundSelected.SetContentMarginOverride(StyleBox.Margin.Vertical, 2); itemListBackgroundSelected.SetContentMarginOverride(StyleBox.Margin.Horizontal, 4); - var itemListItemBackgroundDisabled = new StyleBoxFlat {BackgroundColor = new Color(10, 10, 12)}; + var itemListItemBackgroundDisabled = new StyleBoxFlat { BackgroundColor = new Color(10, 10, 12) }; itemListItemBackgroundDisabled.SetContentMarginOverride(StyleBox.Margin.Vertical, 2); itemListItemBackgroundDisabled.SetContentMarginOverride(StyleBox.Margin.Horizontal, 4); - var itemListItemBackground = new StyleBoxFlat {BackgroundColor = new Color(55, 55, 68)}; + var itemListItemBackground = new StyleBoxFlat { BackgroundColor = new Color(55, 55, 68) }; itemListItemBackground.SetContentMarginOverride(StyleBox.Margin.Vertical, 2); itemListItemBackground.SetContentMarginOverride(StyleBox.Margin.Horizontal, 4); - var itemListItemBackgroundTransparent = new StyleBoxFlat {BackgroundColor = Color.Transparent}; + var itemListItemBackgroundTransparent = new StyleBoxFlat { BackgroundColor = Color.Transparent }; itemListItemBackgroundTransparent.SetContentMarginOverride(StyleBox.Margin.Vertical, 2); itemListItemBackgroundTransparent.SetContentMarginOverride(StyleBox.Margin.Horizontal, 4); @@ -489,9 +489,9 @@ namespace Content.Client.Stylesheets sliderForeBox.SetPatchMargin(StyleBox.Margin.All, 12); sliderGrabBox.SetPatchMargin(StyleBox.Margin.All, 12); - var sliderFillGreen = new StyleBoxTexture(sliderFillBox) {Modulate = Color.LimeGreen}; - var sliderFillRed = new StyleBoxTexture(sliderFillBox) {Modulate = Color.Red}; - var sliderFillBlue = new StyleBoxTexture(sliderFillBox) {Modulate = Color.Blue}; + var sliderFillGreen = new StyleBoxTexture(sliderFillBox) { Modulate = Color.LimeGreen }; + var sliderFillRed = new StyleBoxTexture(sliderFillBox) { Modulate = Color.Red }; + var sliderFillBlue = new StyleBoxTexture(sliderFillBox) { Modulate = Color.Blue }; var sliderFillWhite = new StyleBoxTexture(sliderFillBox) { Modulate = Color.White }; var boxFont13 = resCache.GetFont("/Fonts/Boxfont-round/Boxfont Round.ttf", 13); @@ -1468,6 +1468,25 @@ namespace Content.Client.Stylesheets Element