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:
chromiumboy
2024-03-30 23:29:47 -05:00
committed by GitHub
parent 4d2aa1a70a
commit 02273ca0e7
100 changed files with 2765 additions and 380 deletions

View File

@@ -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);

View 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;
}
}

View 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);
}
}

View 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>

View 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()
{
}
}

View 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();
}
}

View File

@@ -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)

View 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,
}
}

View 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;
}
}