[Feat] RadialContainer (#177)

* Added radial button 🥳

* Added animations 🔔

* Added network radial classes (dont forget remove InternalsSystem.cs, i mean my test hook, And StorageBoundUserInterface.cs

* Functionality meow 🏛️

* Фиксы

* More tests

* Support now more 8 buttons

* Update StorageBoundUserInterface.cs
This commit is contained in:
DocNight
2023-06-25 18:19:34 +03:00
committed by Aviu00
parent ecadeaa5f3
commit 96ae4a31d8
10 changed files with 1161 additions and 0 deletions

View File

@@ -0,0 +1,240 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Client.Examine;
using Content.Client.Gameplay;
using Content.Client.Popups;
using Content.Shared.Examine;
using Content.Shared.Tag;
using Content.Shared.Verbs;
using Content.Shared.White.Radials;
using Content.Shared.White.Radials.Systems;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Client.State;
using Robust.Shared.Map;
using Robust.Shared.Utility;
namespace Content.Client.White.Radials;
[UsedImplicitly]
public sealed class RadialSystem : SharedRadialSystem
{
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly ExamineSystem _examineSystem = default!;
[Dependency] private readonly TagSystem _tagSystem = default!;
[Dependency] private readonly IStateManager _stateManager = default!;
[Dependency] private readonly EntityLookupSystem _entityLookup = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
/// <summary>
/// When a user right clicks somewhere, how large is the box we use to get entities for the context menu?
/// </summary>
public const float EntityMenuLookupSize = 0.25f;
[Dependency] private readonly IEyeManager _eyeManager = default!;
/// <summary>
/// These flags determine what entities the user can see on the context menu.
/// </summary>
public MenuVisibility Visibility;
public Action<RadialsResponseEvent>? OnRadialsResponse;
public override void Initialize()
{
base.Initialize();
SubscribeNetworkEvent<RadialsResponseEvent>(HandleRadialsResponse);
}
/// <summary>
/// Get all of the entities in an area for displaying on the context menu.
/// </summary>
public bool TryGetEntityMenuEntities(MapCoordinates targetPos, [NotNullWhen(true)] out List<EntityUid>? result)
{
result = null;
if (_stateManager.CurrentState is not GameplayStateBase gameScreenBase)
return false;
var player = _playerManager.LocalPlayer?.ControlledEntity;
if (player == null)
return false;
// If FOV drawing is disabled, we will modify the visibility option to ignore visiblity checks.
var visibility = _eyeManager.CurrentEye.DrawFov
? Visibility
: Visibility | MenuVisibility.NoFov;
// Get entities
List<EntityUid> entities;
// Do we have to do FoV checks?
if ((visibility & MenuVisibility.NoFov) == 0)
{
var entitiesUnderMouse = gameScreenBase.GetClickableEntities(targetPos).ToHashSet();
bool Predicate(EntityUid e) => e == player || entitiesUnderMouse.Contains(e);
// first check the general location.
if (!_examineSystem.CanExamine(player.Value, targetPos, Predicate))
return false;
TryComp(player.Value, out ExaminerComponent? examiner);
// Then check every entity
entities = new();
foreach (var ent in _entityLookup.GetEntitiesInRange(targetPos, EntityMenuLookupSize))
{
if (_examineSystem.CanExamine(player.Value, targetPos, Predicate, ent, examiner))
entities.Add(ent);
}
}
else
{
entities = _entityLookup.GetEntitiesInRange(targetPos, EntityMenuLookupSize).ToList();
}
if (entities.Count == 0)
return false;
if (visibility == MenuVisibility.All)
{
result = entities;
return true;
}
// remove any entities in containers
if ((visibility & MenuVisibility.InContainer) == 0)
{
for (var i = entities.Count - 1; i >= 0; i--)
{
var entity = entities[i];
if (ContainerSystem.IsInSameOrTransparentContainer(player.Value, entity))
continue;
entities.RemoveSwap(i);
}
}
// remove any invisible entities
if ((visibility & MenuVisibility.Invisible) == 0)
{
var spriteQuery = GetEntityQuery<SpriteComponent>();
var tagQuery = GetEntityQuery<TagComponent>();
for (var i = entities.Count - 1; i >= 0; i--)
{
var entity = entities[i];
if (!spriteQuery.TryGetComponent(entity, out var spriteComponent) ||
!spriteComponent.Visible ||
_tagSystem.HasTag(entity, "HideContextMenu", tagQuery))
{
entities.RemoveSwap(i);
}
}
}
// Remove any entities that do not have LOS
if ((visibility & MenuVisibility.NoFov) == 0)
{
var xformQuery = GetEntityQuery<TransformComponent>();
var playerPos = xformQuery.GetComponent(player.Value).MapPosition;
for (var i = entities.Count - 1; i >= 0; i--)
{
var entity = entities[i];
if (!ExamineSystemShared.InRangeUnOccluded(
playerPos,
xformQuery.GetComponent(entity).MapPosition,
ExamineSystemShared.ExamineRange,
null))
{
entities.RemoveSwap(i);
}
}
}
if (entities.Count == 0)
return false;
result = entities;
return true;
}
/// <summary>
/// Asks the server to send back a list of server-side verbs, for the given verb type.
/// </summary>
public SortedSet<Radial> GetRadials(EntityUid target, EntityUid user, Type type, bool force = false)
{
return GetRadials(target, user, new List<Type>() { type }, force);
}
/// <summary>
/// Ask the server to send back a list of server-side verbs, and for now return an incomplete list of verbs
/// (only those defined locally).
/// </summary>
public SortedSet<Radial> GetRadials(EntityUid target, EntityUid user, List<Type> verbTypes,
bool force = false)
{
if (!target.IsClientSide())
{
RaiseNetworkEvent(new RequestServerRadialsEvent(target, verbTypes, adminRequest: force));
}
// Some admin menu interactions will try get verbs for entities that have not yet been sent to the player.
if (!Exists(target))
return new();
return GetLocalRadials(target, user, verbTypes, force);
}
/// <summary>
/// Execute actions associated with the given verb.
/// </summary>
/// <remarks>
/// Unless this is a client-exclusive verb, this will also tell the server to run the same verb.
/// </remarks>
public void ExecuteRadial(EntityUid target, Radial radial)
{
var user = _playerManager.LocalPlayer?.ControlledEntity;
if (user == null)
return;
// is this verb actually valid?
if (radial.Disabled)
{
// maybe send an informative pop-up message.
if (!string.IsNullOrWhiteSpace(radial.Message))
_popupSystem.PopupEntity(radial.Message, user.Value);
return;
}
if (radial.ClientExclusive || target.IsClientSide())
ExecuteRadial(radial, user.Value, target);
else
EntityManager.RaisePredictiveEvent(new ExecuteRadialEvent(target, radial));
}
private void HandleRadialsResponse(RadialsResponseEvent msg)
{
OnRadialsResponse?.Invoke(msg);
}
}
[Flags]
public enum MenuVisibility
{
// What entities can a user see on the entity menu?
Default = 0, // They can only see entities in FoV.
NoFov = 1 << 0, // They ignore FoV restrictions
InContainer = 1 << 1, // They can see through containers.
Invisible = 1 << 2, // They can see entities without sprites and the "HideContextMenu" tag is ignored.
All = NoFov | InContainer | Invisible
}

View File

@@ -0,0 +1,126 @@
using System.Linq;
using Content.Client.CombatMode;
using Content.Client.ContextMenu.UI;
using Content.Client.Gameplay;
using Content.Client.Resources;
using Content.Client.Verbs;
using Content.Client.Verbs.UI;
using Content.Client.White.UserInterface.Controls;
using Content.Shared.Input;
using Content.Shared.Verbs;
using Content.Shared.White.Radials;
using Robust.Client.Player;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers;
using Robust.Client.Utility;
using Robust.Shared.Input;
namespace Content.Client.White.Radials;
public sealed class RadialUIController : UIController, IOnStateEntered<GameplayState>, IOnStateExited<GameplayState>
{
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
//[Dependency] private readonly ContextMenuUIController _context = default!;
[UISystemDependency] private readonly CombatModeSystem _combatMode = default!;
[UISystemDependency] private readonly RadialSystem _radialSystem = default!;
public EntityUid CurrentTarget;
public SortedSet<Radial> CurrentRadials = new();
/// <summary>
/// Separate from <see cref="ContextMenuUIController.RootMenu"/>, since we can open a verb menu as a submenu
/// of an entity menu element. If that happens, we need to be aware and close it properly.
/// </summary>
public RadialContainer? OpenMenu = null;
public void OnStateEntered(GameplayState state)
{
//_context.OnContextClosed += Close;
_radialSystem.OnRadialsResponse += HandleVerbsResponse;
}
public void OnStateExited(GameplayState state)
{
//_context.OnContextClosed -= Close;
if (_radialSystem != null)
_radialSystem.OnRadialsResponse -= HandleVerbsResponse;
Close();
}
/// <summary>
/// Open a verb menu and fill it with verbs applicable to the given target entity.
/// </summary>
/// <param name="target">Entity to get verbs on.</param>
/// <param name="force">Used to force showing all verbs (mostly for admins).</param>
/// <param name="popup">
/// If this is not null, verbs will be placed into the given popup instead.
/// </param>
public void OpenRadialMenu(EntityUid target, bool force = false)
{
if (_playerManager.LocalPlayer?.ControlledEntity is not {Valid: true} user ||
_combatMode.IsInCombatMode(user))
return;
Close();
CurrentTarget = target;
CurrentRadials = _radialSystem.GetRadials(target, user, Radial.RadialTypes, force);
OpenMenu = new RadialContainer();
OpenMenu.NormalSize = 50;
OpenMenu.FocusSize = 64;
//Feat: Disable action text, while im not fixed it
OpenMenu.IsAction = false;
}
private void FillRadial()
{
OpenMenu ??= new RadialContainer();
OpenMenu.CloseButton.Controller.OnPressed += (_) => Close();
foreach (var radial in CurrentRadials)
{
var button = OpenMenu.AddButton(radial.Text, radial.Icon ?? null);
button.Controller.OnPressed += (_) => { ExecuteRadial(radial); };
}
OpenMenu.Open(_userInterfaceManager.MousePositionScaled.Position);
}
public void AddServerRadials(List<Radial> radials)
{
CurrentRadials.UnionWith(radials);
FillRadial();
}
private void Close()
{
if (OpenMenu == null)
return;
OpenMenu.Close();
OpenMenu = null;
}
private void HandleVerbsResponse(RadialsResponseEvent msg)
{
if (OpenMenu == null || CurrentTarget != msg.Entity)
return;
if (msg.Radials == null)
return;
AddServerRadials(msg.Radials);
}
private void ExecuteRadial(Radial radial)
{
_radialSystem.ExecuteRadial(CurrentTarget, radial);
if (radial.CloseMenu ?? radial.CloseMenuDefault)
Close(); //_context.Close();
}
}

View File

@@ -0,0 +1,18 @@
<Control
xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.White.UserInterface.Controls">
<BoxContainer>
<TextureButton
Name="Controller"
Access="Public"
VerticalExpand="True"
HorizontalExpand="True">
<TextureRect Access="Public" Name="BackgroundTexture"
VerticalExpand="True"
HorizontalExpand="True"
VerticalAlignment="Center" HorizontalAlignment="Center" Stretch="KeepAspect">
</TextureRect>
</TextureButton>
</BoxContainer>
</Control>

View File

@@ -0,0 +1,39 @@
using JetBrains.Annotations;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Animations;
using Robust.Shared.Timing;
namespace Content.Client.White.UserInterface.Controls;
[GenerateTypedNameReferences, Virtual, PublicAPI]
public sealed partial class RadialButton : Control
{
[Animatable] public Vector2 Offset { get; set; }
public string? Content { get; set; }
public string Texture
{
set => Controller.TexturePath = value;
}
[Animatable]
public Vector2 ButtonSize
{
get => this.Size;
set => this.SetSize = value;
}
public RadialButton()
{
RobustXamlLoader.Load(this);
Offset = Vector2.Zero;
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
}
}

View File

@@ -0,0 +1,19 @@
<controls:RadialContainer
xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.White.UserInterface.Controls"
Visible="False"
MaxSize="0 0">
<BoxContainer>
<controls:RadialButton
Name="CloseButton"
Access="Public"
VerticalExpand="True"
HorizontalExpand="True"
Content="Close"
Texture="/Textures/Interface/Default/blocked.png"/>
<LayoutContainer Access="Public" Name="Layout" HorizontalExpand="True" VerticalExpand="True"></LayoutContainer>
<BoxContainer Access="Public" Name="ActionBox" HorizontalExpand="True" VerticalExpand="True">
<Label Access="Public" Name="ActionLabel" HorizontalExpand="True" Visible="False"/>
</BoxContainer>
</BoxContainer>
</controls:RadialContainer>

View File

@@ -0,0 +1,257 @@
using System.Linq;
using Content.Client.Gameplay;
using Content.Client.Resources;
using JetBrains.Annotations;
using Robust.Client.Animations;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Client.Utility;
using Robust.Shared.Animations;
using Robust.Shared.Console;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Client.White.UserInterface.Controls;
public sealed class RadialContainerCommandTest : LocalizedCommands
{
public override string Command => "radialtest";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var radial = new RadialContainer();
for (int i = 0; i < 24; i++)
{
var testButton = radial.AddButton("Action " + i, "/Textures/Interface/emotions.svg.192dpi.png");
testButton.Controller.OnPressed += (_) => { Logger.Debug("Press gay"); };
}
radial.CloseButton.Controller.OnPressed += (_) =>
{
radial.Close();
radial.Dispose();
};
//radial.OpenCentered();
var usrMngr = IoCManager.Resolve<IUserInterfaceManager>();
radial.Open(usrMngr.MousePositionScaled.Position);
}
}
[GenerateTypedNameReferences, Virtual]
public partial class RadialContainer : Control
{
private bool _isOpened = false;
private Vector2 _focusSize = new Vector2(64, 64);
private Vector2 _normalSize = new Vector2(50, 50);
private float _moveAniTime = 0.3f;
private float _focusAniTime = 0.25f;
private int _maxButtons = 8;
private string _backgroundTexture = "/Textures/Interface/Default/SlotBackground.png";
public const string MoveAnimationKey = "move";
public const string InSizeAnimationKey = "insize";
public const string OutSizeAnimationKey = "outsize";
public float FocusSize
{
get => _focusSize.Y;
set => _focusSize = new Vector2(value, value);
}
public float NormalSize
{
get => _normalSize.Y;
set => _normalSize = new Vector2(value, value);
}
public float MoveAnimationTime
{
get => _moveAniTime;
set => _moveAniTime = value;
}
public float FocusAnimationTime
{
get => _focusAniTime;
set => _focusAniTime = value;
}
public bool IsAction = true;
public RadialContainer() : base()
{
RobustXamlLoader.Load(this);
}
public void Open(Vector2 position)
{
AddToRoot();
LayoutContainer.SetPosition(this, position);
UpdateButtons();
}
public void OpenCentered()
{
AddToRoot();
if (Parent != null)
LayoutContainer.SetPosition(this, (Parent.Size/2) - (this.Size/2));
else
LayoutContainer.SetPosition(this, (UserInterfaceManager.MainViewport.Size/2) - (this.Size/2));
UpdateButtons();
}
public void Close()
{
Parent?.RemoveChild(this);
Visible = false;
_isOpened = false;
}
public RadialButton AddButton(string action, string? texture = null)
{
var button = new RadialButton();
button.Content = action;
button.Controller.TextureNormal = IoCManager.Resolve<IResourceCache>().GetTexture(_backgroundTexture);
if (texture != null)
button.BackgroundTexture.Texture = IoCManager.Resolve<IResourceCache>().GetTexture(texture);
Layout.AddChild(button);
return button;
}
private void AddToRoot()
{
if (_isOpened)
return;
UserInterfaceManager.WindowRoot.AddChild(this);
_isOpened = !_isOpened;
}
private void UpdateButtons()
{
Visible = true;
var angleDegrees = 360/Layout.ChildCount;
var stepAngle = -angleDegrees + -90;
var distance = FocusSize * 1.2;
if (Layout.Children.Count() > _maxButtons)
{
for (int i = 0; i < (Layout.Children.Count() - _maxButtons); i++)
{
distance += (NormalSize/3);
}
}
foreach (var child in Layout.Children)
{
var button = (RadialButton)child;
button.ButtonSize = _normalSize;
stepAngle += angleDegrees;
var pos = GetPointFromPolar(stepAngle, distance);
PlayRadialAnimation(button, pos, MoveAnimationKey);
button.Controller.OnMouseEntered += (_) =>
{
PlaySizeAnimation(button, _focusSize, OutSizeAnimationKey, InSizeAnimationKey);
ActionLabel.Text = button.Content ?? string.Empty;
ActionLabel.Visible = IsAction;
};
button.Controller.OnMouseExited += (_) =>
{
PlaySizeAnimation(button, _normalSize, InSizeAnimationKey, OutSizeAnimationKey);
ActionLabel.Visible = false;
};
}
CloseButton.ButtonSize = _normalSize;
CloseButton.Controller.OnMouseEntered += (_) =>
{
PlaySizeAnimation(CloseButton, _focusSize, OutSizeAnimationKey, InSizeAnimationKey);
ActionLabel.Text = CloseButton.Content ?? string.Empty;
ActionLabel.Visible = true;
};
CloseButton.Controller.OnMouseExited += (_) =>
{
PlaySizeAnimation(CloseButton, _normalSize, InSizeAnimationKey, OutSizeAnimationKey);
ActionLabel.Visible = false;
};
}
private void PlayRadialAnimation(Control button, Vector2 pos, string playKey)
{
var anim = new Animation
{
Length = TimeSpan.FromMilliseconds(_moveAniTime * 1000),
AnimationTracks =
{
new AnimationTrackControlProperty
{
Property = nameof(RadialButton.Offset),
InterpolationMode = AnimationInterpolationMode.Linear,
KeyFrames =
{
new AnimationTrackProperty.KeyFrame(new Vector2(0,0), 0f),
new AnimationTrackProperty.KeyFrame(pos, _moveAniTime)
}
}
}
};
if (!button.HasRunningAnimation(playKey))
button.PlayAnimation(anim, playKey);
}
private void PlaySizeAnimation(Control button, Vector2 size, string playKey, string? stopKey)
{
var anim = new Animation
{
Length = TimeSpan.FromMilliseconds(_focusAniTime * 1000),
AnimationTracks =
{
new AnimationTrackControlProperty
{
Property = nameof(RadialButton.ButtonSize),
InterpolationMode = AnimationInterpolationMode.Linear,
KeyFrames =
{
new AnimationTrackProperty.KeyFrame(button.Size, 0f),
new AnimationTrackProperty.KeyFrame(size, _focusAniTime)
}
}
}
};
if (stopKey != null && button.HasRunningAnimation(stopKey))
button.StopAnimation(stopKey);
if (!button.HasRunningAnimation(playKey))
button.PlayAnimation(anim, playKey);
}
protected override void Draw(DrawingHandleScreen handle)
{
base.Draw(handle);
foreach (var child in Layout.Children)
{
var button = (RadialButton)child;
LayoutContainer.SetPosition(child, button.Offset - (button.Size/2));
}
LayoutContainer.SetPosition(CloseButton, CloseButton.Offset - (CloseButton.Size/2));
LayoutContainer.SetPosition(ActionBox, new Vector2(0 - (ActionLabel.Size.X), FocusSize*1.5f));
}
private static Vector2 GetPointFromPolar(double angleDegrees, double distance)
{
var angleRadians = angleDegrees * (Math.PI / 180.0);
var x = distance * Math.Cos(angleRadians);
var y = distance * Math.Sin(angleRadians);
return new Vector2((int)Math.Round(x), (int)Math.Round(y));
}
}