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
This commit is contained in:
@@ -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);
|
||||
|
||||
122
Content.Client/RCD/AlignRCDConstruction.cs
Normal file
122
Content.Client/RCD/AlignRCDConstruction.cs
Normal file
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// This placement mode is not on the engine because it is content specific (i.e., for the RCD)
|
||||
/// </summary>
|
||||
public AlignRCDConstruction(PlacementManager pMan) : base(pMan)
|
||||
{
|
||||
var dependencies = IoCManager.Instance!;
|
||||
_entityManager = dependencies.Resolve<IEntityManager>();
|
||||
_mapManager = dependencies.Resolve<IMapManager>();
|
||||
_playerManager = dependencies.Resolve<IPlayerManager>();
|
||||
_stateManager = dependencies.Resolve<IStateManager>();
|
||||
|
||||
_mapSystem = _entityManager.System<SharedMapSystem>();
|
||||
_rcdSystem = _entityManager.System<RCDSystem>();
|
||||
_transformSystem = _entityManager.System<SharedTransformSystem>();
|
||||
|
||||
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<MapGridComponent>(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<TransformComponent>(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<HandsComponent>(player, out var hands))
|
||||
return false;
|
||||
|
||||
var heldEntity = hands.ActiveHand?.HeldEntity;
|
||||
|
||||
if (!_entityManager.TryGetComponent<RCDComponent>(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;
|
||||
}
|
||||
}
|
||||
78
Content.Client/RCD/RCDConstructionGhostSystem.cs
Normal file
78
Content.Client/RCD/RCDConstructionGhostSystem.cs
Normal file
@@ -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<RCDComponent>(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<HandsComponent>(player, out var hands))
|
||||
return;
|
||||
|
||||
var heldEntity = hands.ActiveHand?.HeldEntity;
|
||||
|
||||
if (!TryComp<RCDComponent>(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);
|
||||
}
|
||||
}
|
||||
47
Content.Client/RCD/RCDMenu.xaml
Normal file
47
Content.Client/RCD/RCDMenu.xaml
Normal file
@@ -0,0 +1,47 @@
|
||||
<ui:RadialMenu xmlns="https://spacestation14.io"
|
||||
xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
xmlns:rcd="clr-namespace:Content.Client.RCD"
|
||||
BackButtonStyleClass="RadialMenuBackButton"
|
||||
CloseButtonStyleClass="RadialMenuCloseButton"
|
||||
VerticalExpand="True"
|
||||
HorizontalExpand="True"
|
||||
MinSize="450 450">
|
||||
|
||||
<!-- Note: The min size of the window just determine how close to the edge of the screen the center of the radial menu can be placed -->
|
||||
<!-- The radial menu will try to open so that its center is located where the player's cursor is currently -->
|
||||
|
||||
<!-- Entry layer (shows main categories) -->
|
||||
<ui:RadialContainer Name="Main" VerticalExpand="True" HorizontalExpand="True" Radius="64" ReserveSpaceForHiddenChildren="False">
|
||||
<ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'rcd-component-walls-and-flooring'}" TargetLayer="WallsAndFlooring" Visible="False">
|
||||
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Radial/RCD/walls_and_flooring.png"/>
|
||||
</ui:RadialMenuTextureButton>
|
||||
<ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'rcd-component-windows-and-grilles'}" TargetLayer="WindowsAndGrilles" Visible="False">
|
||||
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Radial/RCD/windows_and_grilles.png"/>
|
||||
</ui:RadialMenuTextureButton>
|
||||
<ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'rcd-component-airlocks'}" TargetLayer="Airlocks" Visible="False">
|
||||
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Radial/RCD/airlocks.png"/>
|
||||
</ui:RadialMenuTextureButton>
|
||||
<ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'rcd-component-electrical'}" TargetLayer="Electrical" Visible="False">
|
||||
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Radial/RCD/multicoil.png"/>
|
||||
</ui:RadialMenuTextureButton>
|
||||
<ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'rcd-component-lighting'}" TargetLayer="Lighting" Visible="False">
|
||||
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Radial/RCD/lighting.png"/>
|
||||
</ui:RadialMenuTextureButton>
|
||||
</ui:RadialContainer>
|
||||
|
||||
<!-- Walls and flooring -->
|
||||
<ui:RadialContainer Name="WallsAndFlooring" VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
|
||||
|
||||
<!-- Windows and grilles -->
|
||||
<ui:RadialContainer Name="WindowsAndGrilles" VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
|
||||
|
||||
<!-- Airlocks -->
|
||||
<ui:RadialContainer Name="Airlocks" VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
|
||||
|
||||
<!-- Computer and machine frames -->
|
||||
<ui:RadialContainer Name="Electrical" VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
|
||||
|
||||
<!-- Lighting -->
|
||||
<ui:RadialContainer Name="Lighting" VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
|
||||
|
||||
</ui:RadialMenu>
|
||||
137
Content.Client/RCD/RCDMenu.xaml.cs
Normal file
137
Content.Client/RCD/RCDMenu.xaml.cs
Normal file
@@ -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<ProtoId<RCDPrototype>>? SendRCDSystemMessageAction;
|
||||
|
||||
public RCDMenu(EntityUid owner, RCDMenuBoundUserInterface bui)
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
_spriteSystem = _entManager.System<SpriteSystem>();
|
||||
|
||||
// Find the main radial container
|
||||
var main = FindControl<RadialContainer>("Main");
|
||||
|
||||
if (main == null)
|
||||
return;
|
||||
|
||||
// Populate secondary radial containers
|
||||
if (!_entManager.TryGetComponent<RCDComponent>(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<RadialContainer>(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<RCDPrototype> ProtoId { get; set; }
|
||||
|
||||
public RCDMenuButton()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
49
Content.Client/RCD/RCDMenuBoundUserInterface.cs
Normal file
49
Content.Client/RCD/RCDMenuBoundUserInterface.cs
Normal file
@@ -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<RCDPrototype> 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();
|
||||
}
|
||||
}
|
||||
@@ -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<Label>().Class("Disabled")
|
||||
.Prop("font-color", DisabledFore),
|
||||
|
||||
// Radial menu buttons
|
||||
Element<TextureButton>().Class("RadialMenuButton")
|
||||
.Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Radial/button_normal.png")),
|
||||
Element<TextureButton>().Class("RadialMenuButton")
|
||||
.Pseudo(TextureButton.StylePseudoClassHover)
|
||||
.Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Radial/button_hover.png")),
|
||||
|
||||
Element<TextureButton>().Class("RadialMenuCloseButton")
|
||||
.Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Radial/close_normal.png")),
|
||||
Element<TextureButton>().Class("RadialMenuCloseButton")
|
||||
.Pseudo(TextureButton.StylePseudoClassHover)
|
||||
.Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Radial/close_hover.png")),
|
||||
|
||||
Element<TextureButton>().Class("RadialMenuBackButton")
|
||||
.Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Radial/back_normal.png")),
|
||||
Element<TextureButton>().Class("RadialMenuBackButton")
|
||||
.Pseudo(TextureButton.StylePseudoClassHover)
|
||||
.Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Radial/back_hover.png")),
|
||||
|
||||
//PDA - Backgrounds
|
||||
Element<PanelContainer>().Class("PdaContentBackground")
|
||||
.Prop(PanelContainer.StylePropertyPanel, BaseButtonOpenBoth)
|
||||
|
||||
105
Content.Client/UserInterface/Controls/RadialContainer.cs
Normal file
105
Content.Client/UserInterface/Controls/RadialContainer.cs
Normal file
@@ -0,0 +1,105 @@
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Content.Client.UserInterface.Controls;
|
||||
|
||||
[Virtual]
|
||||
public class RadialContainer : LayoutContainer
|
||||
{
|
||||
/// <summary>
|
||||
/// Specifies the anglular range, in radians, in which child elements will be placed.
|
||||
/// The first value denotes the angle at which the first element is to be placed, and
|
||||
/// the second value denotes the angle at which the last element is to be placed.
|
||||
/// Both values must be between 0 and 2 PI radians
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The top of the screen is at 0 radians, and the bottom of the screen is at PI radians
|
||||
/// </remarks>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public Vector2 AngularRange
|
||||
{
|
||||
get
|
||||
{
|
||||
return _angularRange;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
var x = value.X;
|
||||
var y = value.Y;
|
||||
|
||||
x = x > MathF.Tau ? x % MathF.Tau : x;
|
||||
y = y > MathF.Tau ? y % MathF.Tau : y;
|
||||
|
||||
x = x < 0 ? MathF.Tau + x : x;
|
||||
y = y < 0 ? MathF.Tau + y : y;
|
||||
|
||||
_angularRange = new Vector2(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
private Vector2 _angularRange = new Vector2(0f, MathF.Tau - float.Epsilon);
|
||||
|
||||
/// <summary>
|
||||
/// Determines the direction in which child elements will be arranged
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public RAlignment RadialAlignment { get; set; } = RAlignment.Clockwise;
|
||||
|
||||
/// <summary>
|
||||
/// Determines how far from the radial container's center that its child elements will be placed
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float Radius { get; set; } = 100f;
|
||||
|
||||
/// <summary>
|
||||
/// Sets whether the container should reserve a space on the layout for child which are not currently visible
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool ReserveSpaceForHiddenChildren { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// This container arranges its children, evenly separated, in a radial pattern
|
||||
/// </summary>
|
||||
public RadialContainer()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
protected override void Draw(DrawingHandleScreen handle)
|
||||
{
|
||||
var children = ReserveSpaceForHiddenChildren ? Children : Children.Where(x => x.Visible);
|
||||
var childCount = children.Count();
|
||||
|
||||
// Determine the size of the arc, accounting for clockwise and anti-clockwise arrangements
|
||||
var arc = AngularRange.Y - AngularRange.X;
|
||||
arc = (arc < 0) ? MathF.Tau + arc : arc;
|
||||
arc = (RadialAlignment == RAlignment.AntiClockwise) ? MathF.Tau - arc : arc;
|
||||
|
||||
// Account for both circular arrangements and arc-based arrangements
|
||||
var childMod = MathHelper.CloseTo(arc, MathF.Tau, 0.01f) ? 0 : 1;
|
||||
|
||||
// Determine the separation between child elements
|
||||
var sepAngle = arc / (childCount - childMod);
|
||||
sepAngle *= (RadialAlignment == RAlignment.AntiClockwise) ? -1f : 1f;
|
||||
|
||||
// Adjust the positions of all the child elements
|
||||
foreach (var (i, child) in children.Select((x, i) => (i, x)))
|
||||
{
|
||||
var position = new Vector2(Radius * MathF.Sin(AngularRange.X + sepAngle * i) + Width / 2f - child.Width / 2f, -Radius * MathF.Cos(AngularRange.X + sepAngle * i) + Height / 2f - child.Height / 2f);
|
||||
SetPosition(child, position);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the different radial alignment modes
|
||||
/// </summary>
|
||||
/// <seealso cref="RadialAlignment"/>
|
||||
public enum RAlignment : byte
|
||||
{
|
||||
Clockwise,
|
||||
AntiClockwise,
|
||||
}
|
||||
}
|
||||
255
Content.Client/UserInterface/Controls/RadialMenu.cs
Normal file
255
Content.Client/UserInterface/Controls/RadialMenu.cs
Normal file
@@ -0,0 +1,255 @@
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Content.Client.UserInterface.Controls;
|
||||
|
||||
[Virtual]
|
||||
public class RadialMenu : BaseWindow
|
||||
{
|
||||
/// <summary>
|
||||
/// Contextual button used to traverse through previous layers of the radial menu
|
||||
/// </summary>
|
||||
public TextureButton? ContextualButton { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Set a style class to be applied to the contextual button when it is set to move the user back through previous layers of the radial menu
|
||||
/// </summary>
|
||||
public string? BackButtonStyleClass
|
||||
{
|
||||
get
|
||||
{
|
||||
return _backButtonStyleClass;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
_backButtonStyleClass = value;
|
||||
|
||||
if (_path.Count > 0 && ContextualButton != null && _backButtonStyleClass != null)
|
||||
ContextualButton.SetOnlyStyleClass(_backButtonStyleClass);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set a style class to be applied to the contextual button when it will close the radial menu
|
||||
/// </summary>
|
||||
public string? CloseButtonStyleClass
|
||||
{
|
||||
get
|
||||
{
|
||||
return _closeButtonStyleClass;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
_closeButtonStyleClass = value;
|
||||
|
||||
if (_path.Count == 0 && ContextualButton != null && _closeButtonStyleClass != null)
|
||||
ContextualButton.SetOnlyStyleClass(_closeButtonStyleClass);
|
||||
}
|
||||
}
|
||||
|
||||
private List<Control> _path = new();
|
||||
private string? _backButtonStyleClass;
|
||||
private string? _closeButtonStyleClass;
|
||||
|
||||
/// <summary>
|
||||
/// A free floating menu which enables the quick display of one or more radial containers
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only one radial container is visible at a time (each container forming a separate 'layer' within
|
||||
/// the menu), along with a contextual button at the menu center, which will either return the user
|
||||
/// to the previous layer or close the menu if there are no previous layers left to traverse.
|
||||
/// To create a functional radial menu, simply parent one or more named radial containers to it,
|
||||
/// and populate the radial containers with RadialMenuButtons. Setting the TargetLayer field of these
|
||||
/// buttons to the name of a radial conatiner will display the container in question to the user
|
||||
/// whenever it is clicked in additon to any other actions assigned to the button
|
||||
/// </remarks>
|
||||
public RadialMenu()
|
||||
{
|
||||
// Hide all starting children (if any) except the first (this is the active layer)
|
||||
if (ChildCount > 1)
|
||||
{
|
||||
for (int i = 1; i < ChildCount; i++)
|
||||
GetChild(i).Visible = false;
|
||||
}
|
||||
|
||||
// Auto generate a contextual button for moving back through visited layers
|
||||
ContextualButton = new TextureButton()
|
||||
{
|
||||
HorizontalAlignment = HAlignment.Center,
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
SetSize = new Vector2(64f, 64f),
|
||||
};
|
||||
|
||||
ContextualButton.OnButtonUp += _ => ReturnToPreviousLayer();
|
||||
AddChild(ContextualButton);
|
||||
|
||||
// Hide any further add children, unless its promoted to the active layer
|
||||
OnChildAdded += child => child.Visible = (GetCurrentActiveLayer() == child);
|
||||
}
|
||||
|
||||
private Control? GetCurrentActiveLayer()
|
||||
{
|
||||
var children = Children.Where(x => x != ContextualButton);
|
||||
|
||||
if (!children.Any())
|
||||
return null;
|
||||
|
||||
return children.First(x => x.Visible);
|
||||
}
|
||||
|
||||
public bool TryToMoveToNewLayer(string newLayer)
|
||||
{
|
||||
if (newLayer == string.Empty)
|
||||
return false;
|
||||
|
||||
var currentLayer = GetCurrentActiveLayer();
|
||||
|
||||
if (currentLayer == null)
|
||||
return false;
|
||||
|
||||
var result = false;
|
||||
|
||||
foreach (var child in Children)
|
||||
{
|
||||
if (child == ContextualButton)
|
||||
continue;
|
||||
|
||||
// Hide layers which are not of interest
|
||||
if (result == true || child.Name != newLayer)
|
||||
{
|
||||
child.Visible = false;
|
||||
}
|
||||
|
||||
// Show the layer of interest
|
||||
else
|
||||
{
|
||||
child.Visible = true;
|
||||
result = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Update the traversal path
|
||||
if (result)
|
||||
_path.Add(currentLayer);
|
||||
|
||||
// Set the style class of the button
|
||||
if (_path.Count > 0 && ContextualButton != null && BackButtonStyleClass != null)
|
||||
ContextualButton.SetOnlyStyleClass(BackButtonStyleClass);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public void ReturnToPreviousLayer()
|
||||
{
|
||||
// Close the menu if the traversal path is empty
|
||||
if (_path.Count == 0)
|
||||
{
|
||||
Close();
|
||||
return;
|
||||
}
|
||||
|
||||
var lastChild = _path[^1];
|
||||
|
||||
// Hide all children except the contextual button
|
||||
foreach (var child in Children)
|
||||
{
|
||||
if (child != ContextualButton)
|
||||
child.Visible = false;
|
||||
}
|
||||
|
||||
// Make the last visited layer visible, update the path list
|
||||
lastChild.Visible = true;
|
||||
_path.RemoveAt(_path.Count - 1);
|
||||
|
||||
// Set the style class of the button
|
||||
if (_path.Count == 0 && ContextualButton != null && CloseButtonStyleClass != null)
|
||||
ContextualButton.SetOnlyStyleClass(CloseButtonStyleClass);
|
||||
}
|
||||
}
|
||||
|
||||
[Virtual]
|
||||
public class RadialMenuButton : Button
|
||||
{
|
||||
/// <summary>
|
||||
/// Upon clicking this button the radial menu will transition to the named layer
|
||||
/// </summary>
|
||||
public string? TargetLayer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A simple button that can move the user to a different layer within a radial menu
|
||||
/// </summary>
|
||||
public RadialMenuButton()
|
||||
{
|
||||
OnButtonUp += OnClicked;
|
||||
}
|
||||
|
||||
private void OnClicked(ButtonEventArgs args)
|
||||
{
|
||||
if (TargetLayer == null || TargetLayer == string.Empty)
|
||||
return;
|
||||
|
||||
var parent = FindParentMultiLayerContainer(this);
|
||||
|
||||
if (parent == null)
|
||||
return;
|
||||
|
||||
parent.TryToMoveToNewLayer(TargetLayer);
|
||||
}
|
||||
|
||||
private RadialMenu? FindParentMultiLayerContainer(Control control)
|
||||
{
|
||||
foreach (var ancestor in control.GetSelfAndLogicalAncestors())
|
||||
{
|
||||
if (ancestor is RadialMenu)
|
||||
return ancestor as RadialMenu;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
[Virtual]
|
||||
public class RadialMenuTextureButton : TextureButton
|
||||
{
|
||||
/// <summary>
|
||||
/// Upon clicking this button the radial menu will be moved to the named layer
|
||||
/// </summary>
|
||||
public string TargetLayer { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// A simple texture button that can move the user to a different layer within a radial menu
|
||||
/// </summary>
|
||||
public RadialMenuTextureButton()
|
||||
{
|
||||
OnButtonUp += OnClicked;
|
||||
}
|
||||
|
||||
private void OnClicked(ButtonEventArgs args)
|
||||
{
|
||||
if (TargetLayer == string.Empty)
|
||||
return;
|
||||
|
||||
var parent = FindParentMultiLayerContainer(this);
|
||||
|
||||
if (parent == null)
|
||||
return;
|
||||
|
||||
parent.TryToMoveToNewLayer(TargetLayer);
|
||||
}
|
||||
|
||||
private RadialMenu? FindParentMultiLayerContainer(Control control)
|
||||
{
|
||||
foreach (var ancestor in control.GetSelfAndLogicalAncestors())
|
||||
{
|
||||
if (ancestor is RadialMenu)
|
||||
return ancestor as RadialMenu;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user