Actions System + UI (#2710)

Co-authored-by: Vera Aguilera Puerto <6766154+Zumorica@users.noreply.github.com>
This commit is contained in:
chairbender
2020-12-13 14:28:20 -08:00
committed by GitHub
parent fd0df9a00a
commit 7a3c281f60
150 changed files with 7283 additions and 854 deletions

View File

@@ -0,0 +1,100 @@
#nullable enable
using System;
using Content.Client.UserInterface.Stylesheets;
using Content.Shared.Actions;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Interfaces.Timing;
using Robust.Shared.IoC;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Client.UserInterface
{
/// <summary>
/// Tooltip for actions or alerts because they are very similar.
/// </summary>
public class ActionAlertTooltip : PanelContainer
{
private const float TooltipTextMaxWidth = 350;
private readonly RichTextLabel _cooldownLabel;
private readonly IGameTiming _gameTiming;
/// <summary>
/// Current cooldown displayed in this tooltip. Set to null to show no cooldown.
/// </summary>
public (TimeSpan Start, TimeSpan End)? Cooldown { get; set; }
public ActionAlertTooltip(FormattedMessage name, FormattedMessage? desc, string? requires = null)
{
_gameTiming = IoCManager.Resolve<IGameTiming>();
SetOnlyStyleClass(StyleNano.StyleClassTooltipPanel);
VBoxContainer vbox;
AddChild(vbox = new VBoxContainer {RectClipContent = true});
var nameLabel = new RichTextLabel
{
MaxWidth = TooltipTextMaxWidth,
StyleClasses = {StyleNano.StyleClassTooltipActionTitle}
};
nameLabel.SetMessage(name);
vbox.AddChild(nameLabel);
if (desc != null && !string.IsNullOrWhiteSpace(desc.ToString()))
{
var description = new RichTextLabel
{
MaxWidth = TooltipTextMaxWidth,
StyleClasses = {StyleNano.StyleClassTooltipActionDescription}
};
description.SetMessage(desc);
vbox.AddChild(description);
}
vbox.AddChild(_cooldownLabel = new RichTextLabel
{
MaxWidth = TooltipTextMaxWidth,
StyleClasses = {StyleNano.StyleClassTooltipActionCooldown},
Visible = false
});
if (!string.IsNullOrWhiteSpace(requires))
{
var requiresLabel = new RichTextLabel
{
MaxWidth = TooltipTextMaxWidth,
StyleClasses = {StyleNano.StyleClassTooltipActionRequirements}
};
requiresLabel.SetMessage(FormattedMessage.FromMarkup("[color=#635c5c]" +
requires +
"[/color]"));
vbox.AddChild(requiresLabel);
}
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
if (!Cooldown.HasValue)
{
_cooldownLabel.Visible = false;
return;
}
var timeLeft = Cooldown.Value.End - _gameTiming.CurTime;
if (timeLeft > TimeSpan.Zero)
{
var duration = Cooldown.Value.End - Cooldown.Value.Start;
_cooldownLabel.SetMessage(FormattedMessage.FromMarkup(
$"[color=#a10505]{duration.Seconds} sec cooldown ({timeLeft.Seconds + 1} sec remaining)[/color]"));
_cooldownLabel.Visible = true;
}
else
{
_cooldownLabel.Visible = false;
}
}
}
}

View File

@@ -0,0 +1,499 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Content.Client.GameObjects.Components.Mobs;
using Content.Client.GameObjects.Components.Mobs.Actions;
using Content.Client.UserInterface.Controls;
using Content.Client.UserInterface.Stylesheets;
using Content.Client.Utility;
using Content.Shared.Actions;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.Utility;
using Robust.Shared.Input;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Timing;
namespace Content.Client.UserInterface
{
/// <summary>
/// Action selection menu, allows filtering and searching over all possible
/// actions and populating those actions into the hotbar.
/// </summary>
public class ActionMenu : SS14Window
{
private const string ItemTag = "item";
private const string NotItemTag = "not item";
private const string InstantActionTag = "instant";
private const string ToggleActionTag = "toggle";
private const string TargetActionTag = "target";
private const string AllActionsTag = "all";
private const string GrantedActionsTag = "granted";
private const int MinSearchLength = 3;
private static readonly Regex NonAlphanumeric = new Regex(@"\W", RegexOptions.Compiled);
private static readonly Regex Whitespace = new Regex(@"\s+", RegexOptions.Compiled);
private static readonly BaseActionPrototype[] EmptyActionList = Array.Empty<BaseActionPrototype>();
// parallel list of actions currently selectable in itemList
private BaseActionPrototype[] _actionList;
private readonly ActionManager _actionManager;
private readonly ClientActionsComponent _actionsComponent;
private readonly ActionsUI _actionsUI;
private readonly LineEdit _searchBar;
private readonly MultiselectOptionButton<string> _filterButton;
private readonly Label _filterLabel;
private readonly Button _clearButton;
private readonly GridContainer _resultsGrid;
private readonly TextureRect _dragShadow;
private readonly DragDropHelper<ActionMenuItem> _dragDropHelper;
public ActionMenu(ClientActionsComponent actionsComponent, ActionsUI actionsUI)
{
_actionsComponent = actionsComponent;
_actionsUI = actionsUI;
_actionManager = IoCManager.Resolve<ActionManager>();
Title = Loc.GetString("Actions");
CustomMinimumSize = (300, 300);
Contents.AddChild(new VBoxContainer
{
Children =
{
new HBoxContainer
{
Children =
{
(_searchBar = new LineEdit
{
StyleClasses = { StyleNano.StyleClassActionSearchBox },
SizeFlagsHorizontal = SizeFlags.FillExpand,
PlaceHolder = Loc.GetString("Search")
}),
(_filterButton = new MultiselectOptionButton<string>()
{
Label = Loc.GetString("Filter")
}),
}
},
(_clearButton = new Button
{
Text = Loc.GetString("Clear"),
}),
(_filterLabel = new Label()),
new ScrollContainer
{
//TODO: needed? CustomMinimumSize = new Vector2(200.0f, 0.0f),
SizeFlagsVertical = SizeFlags.FillExpand,
SizeFlagsHorizontal = SizeFlags.FillExpand,
Children =
{
(_resultsGrid = new GridContainer
{
MaxWidth = 300
})
}
}
}
});
// populate filters from search tags
var filterTags = new List<string>();
foreach (var action in _actionManager.EnumerateActions())
{
filterTags.AddRange(action.Filters);
}
// special one to filter to only include item actions
filterTags.Add(ItemTag);
filterTags.Add(NotItemTag);
filterTags.Add(InstantActionTag);
filterTags.Add(ToggleActionTag);
filterTags.Add(TargetActionTag);
filterTags.Add(AllActionsTag);
filterTags.Add(GrantedActionsTag);
foreach (var tag in filterTags.Distinct().OrderBy(tag => tag))
{
_filterButton.AddItem( CultureInfo.CurrentCulture.TextInfo.ToTitleCase(tag), tag);
}
UpdateFilterLabel();
_dragShadow = new TextureRect
{
CustomMinimumSize = (64, 64),
Stretch = TextureRect.StretchMode.Scale,
Visible = false
};
UserInterfaceManager.PopupRoot.AddChild(_dragShadow);
LayoutContainer.SetSize(_dragShadow, (64, 64));
_dragDropHelper = new DragDropHelper<ActionMenuItem>(OnBeginActionDrag, OnContinueActionDrag, OnEndActionDrag);
}
protected override void EnteredTree()
{
base.EnteredTree();
_clearButton.OnPressed += OnClearButtonPressed;
_searchBar.OnTextChanged += OnSearchTextChanged;
_filterButton.OnItemSelected += OnFilterItemSelected;
foreach (var actionMenuControl in _resultsGrid.Children)
{
var actionMenuItem = (actionMenuControl as ActionMenuItem);
actionMenuItem.OnButtonDown += OnItemButtonDown;
actionMenuItem.OnButtonUp += OnItemButtonUp;
actionMenuItem.OnPressed += OnItemPressed;
}
}
protected override void ExitedTree()
{
base.ExitedTree();
_clearButton.OnPressed -= OnClearButtonPressed;
_searchBar.OnTextChanged -= OnSearchTextChanged;
_filterButton.OnItemSelected -= OnFilterItemSelected;
foreach (var actionMenuControl in _resultsGrid.Children)
{
var actionMenuItem = (actionMenuControl as ActionMenuItem);
actionMenuItem.OnButtonDown -= OnItemButtonDown;
actionMenuItem.OnButtonUp -= OnItemButtonUp;
actionMenuItem.OnPressed -= OnItemPressed;
}
}
private void OnFilterItemSelected(MultiselectOptionButton<string>.ItemPressedEventArgs args)
{
UpdateFilterLabel();
SearchAndDisplay();
}
protected override void Resized()
{
base.Resized();
// TODO: Can rework this once https://github.com/space-wizards/RobustToolbox/issues/1392 is done,
// currently no good way to let the grid know what size it has to "work with", so must manually resize
_resultsGrid.MaxWidth = Width;
}
private bool OnBeginActionDrag()
{
_dragShadow.Texture = _dragDropHelper.Dragged.Action.Icon.Frame0();
// don't make visible until frameupdate, otherwise it'll flicker
LayoutContainer.SetPosition(_dragShadow, UserInterfaceManager.MousePositionScaled - (32, 32));
return true;
}
private bool OnContinueActionDrag(float frameTime)
{
// keep dragged entity centered under mouse
LayoutContainer.SetPosition(_dragShadow, UserInterfaceManager.MousePositionScaled - (32, 32));
// we don't set this visible until frameupdate, otherwise it flickers
_dragShadow.Visible = true;
return true;
}
private void OnEndActionDrag()
{
_dragShadow.Visible = false;
}
private void OnItemButtonDown(BaseButton.ButtonEventArgs args)
{
if (args.Event.Function != EngineKeyFunctions.UIClick) return;
_dragDropHelper.MouseDown(args.Button as ActionMenuItem);
}
private void OnItemButtonUp(BaseButton.ButtonEventArgs args)
{
// note the buttonup only fires on the control that was originally
// pressed to initiate the drag, NOT the one we are currently hovering
if (args.Event.Function != EngineKeyFunctions.UIClick) return;
if (UserInterfaceManager.CurrentlyHovered is ActionSlot targetSlot)
{
if (!_dragDropHelper.IsDragging || _dragDropHelper.Dragged?.Action == null)
{
_dragDropHelper.EndDrag();
return;
}
// drag and drop
switch (_dragDropHelper.Dragged.Action)
{
// assign the dragged action to the target slot
case ActionPrototype actionPrototype:
_actionsComponent.Assignments.AssignSlot(_actionsUI.SelectedHotbar, targetSlot.SlotIndex, ActionAssignment.For(actionPrototype.ActionType));
break;
case ItemActionPrototype itemActionPrototype:
// the action menu doesn't show us if the action has an associated item,
// so when we perform the assignment, we should check if we currently have an unassigned state
// for this item and assign it tied to that item if so, otherwise assign it "itemless"
// this is not particularly efficient but we don't maintain an index from
// item action type to its action states, and this method should be pretty infrequent so it's probably fine
var assigned = false;
foreach (var (item, itemStates) in _actionsComponent.ItemActionStates())
{
foreach (var (actionType, _) in itemStates)
{
if (actionType != itemActionPrototype.ActionType) continue;
var assignment = ActionAssignment.For(actionType, item);
if (_actionsComponent.Assignments.HasAssignment(assignment)) continue;
// no assignment for this state, assign tied to the item
assigned = true;
_actionsComponent.Assignments.AssignSlot(_actionsUI.SelectedHotbar, targetSlot.SlotIndex, assignment);
break;
}
if (assigned)
{
break;
}
}
if (!assigned)
{
_actionsComponent.Assignments.AssignSlot(_actionsUI.SelectedHotbar, targetSlot.SlotIndex, ActionAssignment.For(itemActionPrototype.ActionType));
}
break;
}
_actionsUI.UpdateUI();
}
_dragDropHelper.EndDrag();
}
private void OnItemPressed(BaseButton.ButtonEventArgs args)
{
if (args.Button is not ActionMenuItem actionMenuItem) return;
switch (actionMenuItem.Action)
{
case ActionPrototype actionPrototype:
_actionsComponent.Assignments.AutoPopulate(ActionAssignment.For(actionPrototype.ActionType), _actionsUI.SelectedHotbar);
break;
case ItemActionPrototype itemActionPrototype:
_actionsComponent.Assignments.AutoPopulate(ActionAssignment.For(itemActionPrototype.ActionType), _actionsUI.SelectedHotbar);
break;
default:
Logger.ErrorS("action", "unexpected action prototype {0}", actionMenuItem.Action);
break;
}
_actionsUI.UpdateUI();
}
private void OnClearButtonPressed(BaseButton.ButtonEventArgs args)
{
_searchBar.Clear();
_filterButton.DeselectAll();
UpdateFilterLabel();
SearchAndDisplay();
}
private void OnSearchTextChanged(LineEdit.LineEditEventArgs obj)
{
SearchAndDisplay();
}
private void SearchAndDisplay()
{
var search = Standardize(_searchBar.Text);
// only display nothing if there are no filters selected and text is not long enough.
// otherwise we will search if even one filter is applied, regardless of length of search string
if (_filterButton.SelectedKeys.Count == 0 &&
(string.IsNullOrWhiteSpace(search) || search.Length < MinSearchLength))
{
ClearList();
return;
}
var matchingActions = _actionManager.EnumerateActions()
.Where(a => MatchesSearchCriteria(a, search, _filterButton.SelectedKeys));
PopulateActions(matchingActions);
}
private void UpdateFilterLabel()
{
if (_filterButton.SelectedKeys.Count == 0)
{
_filterLabel.Visible = false;
}
else
{
_filterLabel.Visible = true;
_filterLabel.Text = Loc.GetString("Filters: {0}", string.Join(", ", _filterButton.SelectedLabels));
}
}
private bool MatchesSearchCriteria(BaseActionPrototype action, string standardizedSearch,
IReadOnlyList<string> selectedFilterTags)
{
// check filter tag match first - each action must contain all filter tags currently selected.
// if no tags selected, don't check tags
if (selectedFilterTags.Count > 0 && selectedFilterTags.Any(filterTag => !ActionMatchesFilterTag(action, filterTag)))
{
return false;
}
// check search tag match against the search query
if (action.Keywords.Any(standardizedSearch.Contains))
{
return true;
}
if (Standardize(ActionTypeString(action)).Contains(standardizedSearch))
{
return true;
}
// allows matching by typing spaces between the enum case changes, like "xeno spit" if the
// actiontype is "XenoSpit"
if (Standardize(ActionTypeString(action), true).Contains(standardizedSearch))
{
return true;
}
if (Standardize(action.Name.ToString()).Contains(standardizedSearch))
{
return true;
}
return false;
}
private string ActionTypeString(BaseActionPrototype baseActionPrototype)
{
if (baseActionPrototype is ActionPrototype actionPrototype)
{
return actionPrototype.ActionType.ToString();
}
if (baseActionPrototype is ItemActionPrototype itemActionPrototype)
{
return itemActionPrototype.ActionType.ToString();
}
throw new InvalidOperationException();
}
private bool ActionMatchesFilterTag(BaseActionPrototype action, string tag)
{
return tag switch
{
AllActionsTag => true,
GrantedActionsTag => _actionsComponent.IsGranted(action),
ItemTag => action is ItemActionPrototype,
NotItemTag => action is ActionPrototype,
InstantActionTag => action.BehaviorType == BehaviorType.Instant,
TargetActionTag => action.BehaviorType == BehaviorType.TargetEntity ||
action.BehaviorType == BehaviorType.TargetPoint,
ToggleActionTag => action.BehaviorType == BehaviorType.Toggle,
_ => action.Filters.Contains(tag)
};
}
/// <summary>
/// Standardized form is all lowercase, no non-alphanumeric characters (converted to whitespace),
/// trimmed, 1 space max per whitespace gap,
/// and optional spaces between case change
/// </summary>
private static string Standardize(string rawText, bool splitOnCaseChange = false)
{
rawText ??= "";
// treat non-alphanumeric characters as whitespace
rawText = NonAlphanumeric.Replace(rawText, " ");
// trim spaces and reduce internal whitespaces to 1 max
rawText = Whitespace.Replace(rawText, " ").Trim();
if (splitOnCaseChange)
{
// insert a space when case switches from lower to upper
rawText = AddSpaces(rawText, true);
}
return rawText.ToLowerInvariant().Trim();
}
// taken from https://stackoverflow.com/a/272929 (CC BY-SA 3.0)
private static string AddSpaces(string text, bool preserveAcronyms)
{
if (string.IsNullOrWhiteSpace(text))
return string.Empty;
var newText = new StringBuilder(text.Length * 2);
newText.Append(text[0]);
for (var i = 1; i < text.Length; i++)
{
if (char.IsUpper(text[i]))
{
if ((text[i - 1] != ' ' && !char.IsUpper(text[i - 1])) ||
(preserveAcronyms && char.IsUpper(text[i - 1]) &&
i < text.Length - 1 && !char.IsUpper(text[i + 1])))
newText.Append(' ');
}
newText.Append(text[i]);
}
return newText.ToString();
}
private void PopulateActions(IEnumerable<BaseActionPrototype> actions)
{
ClearList();
_actionList = actions.ToArray();
foreach (var action in _actionList.OrderBy(act => act.Name.ToString()))
{
var actionItem = new ActionMenuItem(action);
_resultsGrid.Children.Add(actionItem);
actionItem.SetActionState(_actionsComponent.IsGranted(action));
actionItem.OnButtonDown += OnItemButtonDown;
actionItem.OnButtonUp += OnItemButtonUp;
actionItem.OnPressed += OnItemPressed;
}
}
private void ClearList()
{
// TODO: Not sure if this unsub is needed if children are all being cleared
foreach (var actionItem in _resultsGrid.Children)
{
((ActionMenuItem) actionItem).OnPressed -= OnItemPressed;
}
_resultsGrid.Children.Clear();
_actionList = EmptyActionList;
}
/// <summary>
/// Should be invoked when action states change, ensures
/// currently displayed actions are properly showing their revoked / granted status
/// </summary>
public void UpdateUI()
{
foreach (var actionItem in _resultsGrid.Children)
{
var actionMenuItem = ((ActionMenuItem) actionItem);
actionMenuItem.SetActionState(_actionsComponent.IsGranted(actionMenuItem.Action));
}
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.Update(args);
_dragDropHelper.Update(args.DeltaSeconds);
}
}
}

View File

@@ -0,0 +1,68 @@
#nullable enable
using Content.Client.UserInterface.Stylesheets;
using Content.Shared.Actions;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.Utility;
namespace Content.Client.UserInterface
{
/// <summary>
/// An individual action visible in the action menu.
/// </summary>
public class ActionMenuItem : ContainerButton
{
// shorter than default tooltip delay so user can
// quickly explore what each action is
private const float CustomTooltipDelay = 0.2f;
public BaseActionPrototype Action { get; private set; }
public ActionMenuItem(BaseActionPrototype action)
{
Action = action;
CustomMinimumSize = (64, 64);
SizeFlagsVertical = SizeFlags.None;
AddChild(new TextureRect
{
SizeFlagsHorizontal = SizeFlags.FillExpand,
SizeFlagsVertical = SizeFlags.FillExpand,
Stretch = TextureRect.StretchMode.Scale,
Texture = action.Icon.Frame0()
});
TooltipDelay = CustomTooltipDelay;
TooltipSupplier = SupplyTooltip;
}
private Control SupplyTooltip(Control? sender)
{
return new ActionAlertTooltip(Action.Name, Action.Description, Action.Requires);
}
/// <summary>
/// Change how this is displayed depending on if it is granted or revoked
/// </summary>
public void SetActionState(bool granted)
{
if (granted)
{
if (HasStyleClass(StyleNano.StyleClassActionMenuItemRevoked))
{
RemoveStyleClass(StyleNano.StyleClassActionMenuItemRevoked);
}
}
else
{
if (!HasStyleClass(StyleNano.StyleClassActionMenuItemRevoked))
{
AddStyleClass(StyleNano.StyleClassActionMenuItemRevoked);
}
}
}
}
}

View File

@@ -0,0 +1,556 @@
#nullable enable
using System.Collections.Generic;
using Content.Client.GameObjects.Components.Mobs;
using Content.Client.GameObjects.Components.Mobs.Actions;
using Content.Client.UserInterface.Controls;
using Content.Client.UserInterface.Stylesheets;
using Content.Client.Utility;
using Content.Shared.Actions;
using Robust.Client.Graphics;
using Robust.Client.Interfaces.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.Utility;
using Robust.Shared.GameObjects;
using Robust.Shared.Input;
using Robust.Shared.Input.Binding;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Timing;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
namespace Content.Client.UserInterface
{
/// <summary>
/// The action hotbar on the left side of the screen.
/// </summary>
public sealed class ActionsUI : Container
{
private readonly ClientActionsComponent _actionsComponent;
private readonly ActionManager _actionManager;
private readonly IEntityManager _entityManager;
private readonly IGameTiming _gameTiming;
private readonly ActionSlot[] _slots;
private readonly GridContainer _slotContainer;
private readonly TextureButton _lockButton;
private readonly TextureButton _settingsButton;
private readonly TextureButton _previousHotbarButton;
private readonly Label _loadoutNumber;
private readonly TextureButton _nextHotbarButton;
private readonly Texture _lockTexture;
private readonly Texture _unlockTexture;
private readonly TextureRect _dragShadow;
private readonly ActionMenu _menu;
/// <summary>
/// Index of currently selected hotbar
/// </summary>
public byte SelectedHotbar { get; private set; }
/// <summary>
/// Action slot we are currently selecting a target for.
/// </summary>
public ActionSlot? SelectingTargetFor { get; private set; }
/// <summary>
/// Drag drop helper for coordinating drag drops between action slots
/// </summary>
public DragDropHelper<ActionSlot> DragDropHelper { get; }
/// <summary>
/// Whether the bar is currently locked by the user. This is intended to prevent drag / drop
/// and right click clearing slots. Anything else is still doable.
/// </summary>
public bool Locked { get; private set; }
/// <summary>
/// All the action slots in order.
/// </summary>
public IEnumerable<ActionSlot> Slots => _slots;
public ActionsUI(ClientActionsComponent actionsComponent)
{
_actionsComponent = actionsComponent;
_actionManager = IoCManager.Resolve<ActionManager>();
_entityManager = IoCManager.Resolve<IEntityManager>();
_gameTiming = IoCManager.Resolve<IGameTiming>();
_menu = new ActionMenu(_actionsComponent, this);
LayoutContainer.SetGrowHorizontal(this, LayoutContainer.GrowDirection.End);
LayoutContainer.SetGrowVertical(this, LayoutContainer.GrowDirection.End);
LayoutContainer.SetAnchorTop(this, 0f);
LayoutContainer.SetAnchorBottom(this, 0.8f);
LayoutContainer.SetMarginLeft(this, 10);
LayoutContainer.SetMarginTop(this, 100);
SizeFlagsHorizontal = SizeFlags.None;
SizeFlagsVertical = SizeFlags.FillExpand;
var resourceCache = IoCManager.Resolve<IResourceCache>();
// everything needs to go within an inner panel container so the panel resizes to fit the elements.
// Because ActionsUI is being anchored by layoutcontainer, the hotbar backing would appear too tall
// if ActionsUI was the panel container
var panelContainer = new PanelContainer()
{
StyleClasses = {StyleNano.StyleClassHotbarPanel},
SizeFlagsHorizontal = SizeFlags.None,
SizeFlagsVertical = SizeFlags.None
};
AddChild(panelContainer);
var hotbarContainer = new VBoxContainer
{
SeparationOverride = 3,
SizeFlagsHorizontal = SizeFlags.None
};
panelContainer.AddChild(hotbarContainer);
var settingsContainer = new HBoxContainer
{
SizeFlagsHorizontal = SizeFlags.FillExpand
};
hotbarContainer.AddChild(settingsContainer);
settingsContainer.AddChild(new Control { SizeFlagsHorizontal = SizeFlags.FillExpand, SizeFlagsStretchRatio = 1 });
_lockTexture = resourceCache.GetTexture("/Textures/Interface/Nano/lock.svg.png");
_unlockTexture = resourceCache.GetTexture("/Textures/Interface/Nano/lock_open.svg.png");
_lockButton = new TextureButton
{
TextureNormal = _unlockTexture,
SizeFlagsHorizontal = SizeFlags.ShrinkCenter,
SizeFlagsVertical = SizeFlags.ShrinkCenter,
SizeFlagsStretchRatio = 1
};
settingsContainer.AddChild(_lockButton);
settingsContainer.AddChild(new Control { SizeFlagsHorizontal = SizeFlags.FillExpand, SizeFlagsStretchRatio = 2 });
_settingsButton = new TextureButton
{
TextureNormal = resourceCache.GetTexture("/Textures/Interface/Nano/gear.svg.png"),
SizeFlagsHorizontal = SizeFlags.ShrinkCenter,
SizeFlagsVertical = SizeFlags.ShrinkCenter,
SizeFlagsStretchRatio = 1
};
settingsContainer.AddChild(_settingsButton);
settingsContainer.AddChild(new Control { SizeFlagsHorizontal = SizeFlags.FillExpand, SizeFlagsStretchRatio = 1 });
// this allows a 2 column layout if window gets too small
_slotContainer = new GridContainer
{
MaxHeight = CalcMaxHeight()
};
hotbarContainer.AddChild(_slotContainer);
var loadoutContainer = new HBoxContainer
{
SizeFlagsHorizontal = SizeFlags.FillExpand
};
hotbarContainer.AddChild(loadoutContainer);
loadoutContainer.AddChild(new Control { SizeFlagsHorizontal = SizeFlags.FillExpand, SizeFlagsStretchRatio = 1 });
_previousHotbarButton = new TextureButton
{
TextureNormal = resourceCache.GetTexture("/Textures/Interface/Nano/left_arrow.svg.png"),
SizeFlagsHorizontal = SizeFlags.ShrinkCenter,
SizeFlagsVertical = SizeFlags.ShrinkCenter,
SizeFlagsStretchRatio = 1
};
loadoutContainer.AddChild(_previousHotbarButton);
loadoutContainer.AddChild(new Control { SizeFlagsHorizontal = SizeFlags.FillExpand, SizeFlagsStretchRatio = 2 });
_loadoutNumber = new Label
{
Text = "1",
SizeFlagsStretchRatio = 1
};
loadoutContainer.AddChild(_loadoutNumber);
loadoutContainer.AddChild(new Control { SizeFlagsHorizontal = SizeFlags.FillExpand, SizeFlagsStretchRatio = 2 });
_nextHotbarButton = new TextureButton
{
TextureNormal = resourceCache.GetTexture("/Textures/Interface/Nano/right_arrow.svg.png"),
SizeFlagsHorizontal = SizeFlags.ShrinkCenter,
SizeFlagsVertical = SizeFlags.ShrinkCenter,
SizeFlagsStretchRatio = 1
};
loadoutContainer.AddChild(_nextHotbarButton);
loadoutContainer.AddChild(new Control { SizeFlagsHorizontal = SizeFlags.FillExpand, SizeFlagsStretchRatio = 1 });
_slots = new ActionSlot[ClientActionsComponent.Slots];
_dragShadow = new TextureRect
{
CustomMinimumSize = (64, 64),
Stretch = TextureRect.StretchMode.Scale,
Visible = false
};
UserInterfaceManager.PopupRoot.AddChild(_dragShadow);
LayoutContainer.SetSize(_dragShadow, (64, 64));
for (byte i = 0; i < ClientActionsComponent.Slots; i++)
{
var slot = new ActionSlot(this, actionsComponent, i);
_slotContainer.AddChild(slot);
_slots[i] = slot;
}
DragDropHelper = new DragDropHelper<ActionSlot>(OnBeginActionDrag, OnContinueActionDrag, OnEndActionDrag);
}
protected override void EnteredTree()
{
base.EnteredTree();
_lockButton.OnPressed += OnLockPressed;
_nextHotbarButton.OnPressed += NextHotbar;
_previousHotbarButton.OnPressed += PreviousHotbar;
_settingsButton.OnPressed += OnToggleActionsMenu;
}
protected override void ExitedTree()
{
base.ExitedTree();
StopTargeting();
_menu.Close();
_lockButton.OnPressed -= OnLockPressed;
_nextHotbarButton.OnPressed -= NextHotbar;
_previousHotbarButton.OnPressed -= PreviousHotbar;
_settingsButton.OnPressed -= OnToggleActionsMenu;
}
protected override Vector2 CalculateMinimumSize()
{
// allows us to shrink down to a 2-column layout minimum
return (10, 400);
}
protected override void Resized()
{
base.Resized();
_slotContainer.MaxHeight = CalcMaxHeight();
}
private float CalcMaxHeight()
{
// TODO: Can rework this once https://github.com/space-wizards/RobustToolbox/issues/1392 is done,
// this is here because there isn't currently a good way to allow the grid to adjust its height based
// on constraints, otherwise we would use anchors to lay it out
// it looks bad to have an uneven number of slots in the columns,
// so we either do a single column or 2 equal sized columns
if (Height < 650)
{
// 2 column
return 400;
}
else
{
// 1 column
return 900;
}
}
protected override void UIScaleChanged()
{
_slotContainer.MaxHeight = CalcMaxHeight();
base.UIScaleChanged();
}
/// <summary>
/// Refresh the display of all the slots in the currently displayed hotbar,
/// to reflect the current component state and assignments of actions component.
/// </summary>
public void UpdateUI()
{
_menu.UpdateUI();
foreach (var actionSlot in Slots)
{
var assignedActionType = _actionsComponent.Assignments[SelectedHotbar, actionSlot.SlotIndex];
if (!assignedActionType.HasValue)
{
actionSlot.Clear();
continue;
}
if (assignedActionType.Value.TryGetAction(out var actionType))
{
UpdateActionSlot(actionType, actionSlot, assignedActionType);
}
else if (assignedActionType.Value.TryGetItemActionWithoutItem(out var itemlessActionType))
{
UpdateActionSlot(itemlessActionType, actionSlot, assignedActionType);
}
else if (assignedActionType.Value.TryGetItemActionWithItem(out var itemActionType, out var item))
{
UpdateActionSlot(item, itemActionType, actionSlot, assignedActionType);
}
else
{
Logger.ErrorS("action", "unexpected Assignment type {0}",
assignedActionType.Value.Assignment);
actionSlot.Clear();
}
}
}
private void UpdateActionSlot(ActionType actionType, ActionSlot actionSlot, ActionAssignment? assignedActionType)
{
if (_actionManager.TryGet(actionType, out var action))
{
actionSlot.Assign(action, true);
}
else
{
Logger.ErrorS("action", "unrecognized actionType {0}", assignedActionType);
actionSlot.Clear();
return;
}
if (!_actionsComponent.TryGetActionState(actionType, out var actionState) || !actionState.Enabled)
{
// action is currently disabled
// just revoked an action we were trying to target with, stop targeting
if (SelectingTargetFor?.Action != null && SelectingTargetFor.Action == action)
{
StopTargeting();
}
actionSlot.DisableAction();
actionSlot.Cooldown = null;
}
else
{
// action is currently granted
actionSlot.EnableAction();
actionSlot.Cooldown = actionState.Cooldown;
// if we are targeting with an action now on cooldown, stop targeting
if (SelectingTargetFor?.Action != null && SelectingTargetFor.Action == action &&
actionState.IsOnCooldown(_gameTiming))
{
StopTargeting();
}
}
// check if we need to toggle it
if (action.BehaviorType == BehaviorType.Toggle)
{
actionSlot.ToggledOn = actionState.ToggledOn;
}
}
private void UpdateActionSlot(ItemActionType itemlessActionType, ActionSlot actionSlot,
ActionAssignment? assignedActionType)
{
if (_actionManager.TryGet(itemlessActionType, out var action))
{
actionSlot.Assign(action);
}
else
{
Logger.ErrorS("action", "unrecognized actionType {0}", assignedActionType);
actionSlot.Clear();
}
actionSlot.Cooldown = null;
}
private void UpdateActionSlot(EntityUid item, ItemActionType itemActionType, ActionSlot actionSlot,
ActionAssignment? assignedActionType)
{
if (!_entityManager.TryGetEntity(item, out var itemEntity)) return;
if (_actionManager.TryGet(itemActionType, out var action))
{
actionSlot.Assign(action, itemEntity, true);
}
else
{
Logger.ErrorS("action", "unrecognized actionType {0}", assignedActionType);
actionSlot.Clear();
return;
}
if (!_actionsComponent.TryGetItemActionState(itemActionType, item, out var actionState))
{
// action is no longer tied to an item, this should never happen as we
// check this at the start of this method. But just to be safe
// we will restore our assignment here to the correct state
Logger.ErrorS("action", "coding error, expected actionType {0} to have" +
" a state but it didn't", assignedActionType);
_actionsComponent.Assignments.AssignSlot(SelectedHotbar, actionSlot.SlotIndex,
ActionAssignment.For(itemActionType));
actionSlot.Assign(action);
return;
}
if (!actionState.Enabled)
{
// just disabled an action we were trying to target with, stop targeting
if (SelectingTargetFor?.Action != null && SelectingTargetFor.Action == action)
{
StopTargeting();
}
actionSlot.DisableAction();
}
else
{
// action is currently granted
actionSlot.EnableAction();
// if we are targeting with an action now on cooldown, stop targeting
if (SelectingTargetFor?.Action != null && SelectingTargetFor.Action == action &&
SelectingTargetFor.Item == itemEntity &&
actionState.IsOnCooldown(_gameTiming))
{
StopTargeting();
}
}
actionSlot.Cooldown = actionState.Cooldown;
// check if we need to toggle it
if (action.BehaviorType == BehaviorType.Toggle)
{
actionSlot.ToggledOn = actionState.ToggledOn;
}
}
private void NextHotbar(BaseButton.ButtonEventArgs args)
{
ChangeHotbar((byte) ((SelectedHotbar + 1) % ClientActionsComponent.Hotbars));
}
private void PreviousHotbar(BaseButton.ButtonEventArgs args)
{
var newBar = SelectedHotbar == 0 ? ClientActionsComponent.Hotbars - 1 : SelectedHotbar - 1;
ChangeHotbar((byte) newBar);
}
private void ChangeHotbar(byte hotbar)
{
StopTargeting();
SelectedHotbar = hotbar;
_loadoutNumber.Text = (hotbar + 1).ToString();
UpdateUI();
}
/// <summary>
/// If currently targeting with this slot, stops targeting.
/// If currently targeting with no slot or a different slot, switches to
/// targeting with the specified slot.
/// </summary>
/// <param name="slot"></param>
public void ToggleTargeting(ActionSlot slot)
{
if (SelectingTargetFor == slot)
{
StopTargeting();
return;
}
StartTargeting(slot);
}
/// <summary>
/// Puts us in targeting mode, where we need to pick either a target point or entity
/// </summary>
private void StartTargeting(ActionSlot actionSlot)
{
// If we were targeting something else we should stop
StopTargeting();
SelectingTargetFor = actionSlot;
// show it as toggled on to indicate we are currently selecting a target for it
if (!actionSlot.ToggledOn)
{
actionSlot.ToggledOn = true;
}
}
/// <summary>
/// Switch out of targeting mode if currently selecting target for an action
/// </summary>
public void StopTargeting()
{
if (SelectingTargetFor == null) return;
if (SelectingTargetFor.ToggledOn)
{
SelectingTargetFor.ToggledOn = false;
}
SelectingTargetFor = null;
}
private void OnToggleActionsMenu(BaseButton.ButtonEventArgs args)
{
ToggleActionsMenu();
}
public void ToggleActionsMenu()
{
if (_menu.IsOpen)
{
_menu.Close();
}
else
{
_menu.OpenCentered();
}
}
private void OnLockPressed(BaseButton.ButtonEventArgs obj)
{
Locked = !Locked;
_lockButton.TextureNormal = Locked ? _lockTexture : _unlockTexture;
}
private bool OnBeginActionDrag()
{
// only initiate the drag if the slot has an action in it
if (Locked || DragDropHelper.Dragged.Action == null) return false;
_dragShadow.Texture = DragDropHelper.Dragged.Action.Icon.Frame0();
LayoutContainer.SetPosition(_dragShadow, UserInterfaceManager.MousePositionScaled - (32, 32));
DragDropHelper.Dragged.CancelPress();
return true;
}
private bool OnContinueActionDrag(float frameTime)
{
// stop if there's no action in the slot
if (Locked || DragDropHelper.Dragged.Action == null) return false;
// keep dragged entity centered under mouse
LayoutContainer.SetPosition(_dragShadow, UserInterfaceManager.MousePositionScaled - (32, 32));
// we don't set this visible until frameupdate, otherwise it flickers
_dragShadow.Visible = true;
return true;
}
private void OnEndActionDrag()
{
_dragShadow.Visible = false;
}
/// <summary>
/// Handle keydown / keyup for one of the slots via a keybinding, simulates mousedown/mouseup on it.
/// </summary>
/// <param name="slot">slot index to to receive the press (0 corresponds to the one labeled 1, 9 corresponds to the one labeled 0)</param>
public void HandleHotbarKeybind(byte slot, PointerInputCmdHandler.PointerInputCmdArgs args)
{
var actionSlot = _slots[slot];
actionSlot.Depress(args.State == BoundKeyState.Down);
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.Update(args);
DragDropHelper.Update(args.DeltaSeconds);
}
}
}

View File

@@ -1,10 +1,8 @@
using System;
using Content.Client.UserInterface.Stylesheets;
using Robust.Client.Graphics.Drawing;
using Robust.Client.Interfaces.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Maths;
namespace Content.Client.UserInterface
@@ -16,58 +14,51 @@ namespace Content.Client.UserInterface
{
public GridContainer Grid { get; }
private readonly IClyde _clyde;
public AlertsUI(IClyde clyde)
public AlertsUI()
{
_clyde = clyde;
LayoutContainer.SetGrowHorizontal(this, LayoutContainer.GrowDirection.Begin);
LayoutContainer.SetGrowVertical(this, LayoutContainer.GrowDirection.End);
LayoutContainer.SetAnchorTop(this, 0f);
LayoutContainer.SetAnchorRight(this, 1f);
LayoutContainer.SetAnchorBottom(this, 1f);
LayoutContainer.SetMarginBottom(this, -180);
LayoutContainer.SetMarginTop(this, 250);
LayoutContainer.SetMarginRight(this, -10);
var panelContainer = new PanelContainer
{
StyleClasses = {StyleNano.StyleClassTransparentBorderedWindowPanel},
SizeFlagsVertical = SizeFlags.FillExpand,
SizeFlagsHorizontal = SizeFlags.ShrinkEnd,
SizeFlagsVertical = SizeFlags.None
};
AddChild(panelContainer);
Grid = new GridContainer
{
MaxHeight = CalcMaxHeight(clyde.ScreenSize),
MaxHeight = 64,
ExpandBackwards = true
};
panelContainer.AddChild(Grid);
clyde.OnWindowResized += ClydeOnOnWindowResized;
LayoutContainer.SetGrowHorizontal(this, LayoutContainer.GrowDirection.Begin);
LayoutContainer.SetAnchorAndMarginPreset(this, LayoutContainer.LayoutPreset.TopRight, margin: 10);
LayoutContainer.SetMarginTop(this, 250);
}
protected override void UIScaleChanged()
{
Grid.MaxHeight = CalcMaxHeight(_clyde.ScreenSize);
base.UIScaleChanged();
}
private void ClydeOnOnWindowResized(WindowResizedEventArgs obj)
protected override void Resized()
{
// TODO: Can rework this once https://github.com/space-wizards/RobustToolbox/issues/1392 is done,
// this is here because there isn't currently a good way to allow the grid to adjust its height based
// on constraints, otherwise we would use anchors to lay it out
Grid.MaxHeight = CalcMaxHeight(obj.NewSize);;
base.Resized();
Grid.MaxHeight = Height;
}
private float CalcMaxHeight(Vector2i screenSize)
protected override Vector2 CalculateMinimumSize()
{
return Math.Max(((screenSize.Y) / UIScale) - 420, 1);
// allows us to shrink down to a single row
return (64, 64);
}
protected override void Dispose(bool disposing)
protected override void UIScaleChanged()
{
base.Dispose(disposing);
if (disposing)
{
_clyde.OnWindowResized -= ClydeOnOnWindowResized;
}
Grid.MaxHeight = Height;
base.UIScaleChanged();
}
}
}

View File

@@ -0,0 +1,652 @@
#nullable enable
using System;
using Content.Client.GameObjects.Components.Mobs;
using Content.Client.UserInterface.Stylesheets;
using Content.Shared.Actions;
using Content.Shared.GameObjects.Components.Mobs;
using Robust.Client.Graphics;
using Robust.Client.Interfaces.GameObjects.Components;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.Utility;
using Robust.Shared.Input;
using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.Timing;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Client.UserInterface.Controls
{
/// <summary>
/// A slot in the action hotbar. Not extending BaseButton because
/// its needs diverged too much.
/// </summary>
public class ActionSlot : PanelContainer
{
// shorter than default tooltip delay so user can more easily
// see what actions they've been given
private const float CustomTooltipDelay = 0.5f;
private static readonly string EnabledColor = "#7b7e9e";
private static readonly string DisabledColor = "#950000";
/// <summary>
/// Current action in this slot.
/// </summary>
public BaseActionPrototype? Action { get; private set; }
/// <summary>
/// true if there is an action assigned to the slot
/// </summary>
public bool HasAssignment => Action != null;
private bool HasToggleSprite => Action != null && Action.IconOn != SpriteSpecifier.Invalid;
/// <summary>
/// Only applicable when an action is in this slot.
/// True if the action is currently shown as enabled, false if action disabled.
/// </summary>
public bool ActionEnabled { get; private set; }
/// <summary>
/// Is there an action in the slot that can currently be used?
/// </summary>
public bool CanUseAction => HasAssignment && ActionEnabled && !IsOnCooldown;
/// <summary>
/// Item the action is provided by, only valid if Action is an ItemActionPrototype. May be null
/// if the item action is not yet tied to an item.
/// </summary>
public IEntity? Item { get; private set; }
/// <summary>
/// Whether the action in this slot should be shown as toggled on. Separate from Depressed.
/// </summary>
public bool ToggledOn
{
get => _toggledOn;
set
{
if (_toggledOn == value) return;
_toggledOn = value;
UpdateIcons();
DrawModeChanged();
}
}
/// <summary>
/// 1-10 corresponding to the number label on the slot (10 is labeled as 0)
/// </summary>
private byte SlotNumber => (byte) (SlotIndex + 1);
public byte SlotIndex { get; }
/// <summary>
/// Current cooldown displayed in this slot. Set to null to show no cooldown.
/// </summary>
public (TimeSpan Start, TimeSpan End)? Cooldown
{
get => _cooldown;
set
{
_cooldown = value;
if (SuppliedTooltip is ActionAlertTooltip actionAlertTooltip)
{
actionAlertTooltip.Cooldown = value;
}
}
}
private (TimeSpan Start, TimeSpan End)? _cooldown;
public bool IsOnCooldown => Cooldown.HasValue && _gameTiming.CurTime < Cooldown.Value.End;
private readonly IGameTiming _gameTiming;
private readonly RichTextLabel _number;
private readonly TextureRect _bigActionIcon;
private readonly TextureRect _smallActionIcon;
private readonly SpriteView _smallItemSpriteView;
private readonly SpriteView _bigItemSpriteView;
private readonly CooldownGraphic _cooldownGraphic;
private readonly ActionsUI _actionsUI;
private readonly ClientActionsComponent _actionsComponent;
private bool _toggledOn;
// whether button is currently pressed down by mouse or keybind down.
private bool _depressed;
private bool _beingHovered;
/// <summary>
/// Creates an action slot for the specified number
/// </summary>
/// <param name="slotIndex">slot index this corresponds to, 0-9 (0 labeled as 1, 8, labeled "9", 9 labeled as "0".</param>
public ActionSlot(ActionsUI actionsUI, ClientActionsComponent actionsComponent, byte slotIndex)
{
_actionsComponent = actionsComponent;
_actionsUI = actionsUI;
_gameTiming = IoCManager.Resolve<IGameTiming>();
SlotIndex = slotIndex;
MouseFilter = MouseFilterMode.Stop;
CustomMinimumSize = (64, 64);
SizeFlagsVertical = SizeFlags.None;
TooltipDelay = CustomTooltipDelay;
TooltipSupplier = SupplyTooltip;
_number = new RichTextLabel
{
StyleClasses = {StyleNano.StyleClassHotbarSlotNumber}
};
_number.SetMessage(SlotNumberLabel());
_bigActionIcon = new TextureRect
{
SizeFlagsHorizontal = SizeFlags.FillExpand,
SizeFlagsVertical = SizeFlags.FillExpand,
Stretch = TextureRect.StretchMode.Scale,
Visible = false
};
_bigItemSpriteView = new SpriteView
{
SizeFlagsHorizontal = SizeFlags.FillExpand,
SizeFlagsVertical = SizeFlags.FillExpand,
Scale = (2,2),
Visible = false
};
_smallActionIcon = new TextureRect
{
SizeFlagsHorizontal = SizeFlags.ShrinkEnd,
SizeFlagsVertical = SizeFlags.ShrinkEnd,
Stretch = TextureRect.StretchMode.Scale,
Visible = false
};
_smallItemSpriteView = new SpriteView
{
SizeFlagsHorizontal = SizeFlags.ShrinkEnd,
SizeFlagsVertical = SizeFlags.ShrinkEnd,
Visible = false
};
_cooldownGraphic = new CooldownGraphic {Progress = 0, Visible = false};
// padding to the left of the number to shift it right
var paddingBox = new HBoxContainer()
{
SizeFlagsHorizontal = SizeFlags.FillExpand,
SizeFlagsVertical = SizeFlags.FillExpand,
CustomMinimumSize = (64, 64)
};
paddingBox.AddChild(new Control()
{
CustomMinimumSize = (4, 4),
SizeFlagsVertical = SizeFlags.Fill
});
paddingBox.AddChild(_number);
// padding to the left of the small icon
var paddingBoxItemIcon = new HBoxContainer()
{
SizeFlagsHorizontal = SizeFlags.FillExpand,
SizeFlagsVertical = SizeFlags.FillExpand,
CustomMinimumSize = (64, 64)
};
paddingBoxItemIcon.AddChild(new Control()
{
CustomMinimumSize = (32, 32),
SizeFlagsVertical = SizeFlags.Fill
});
paddingBoxItemIcon.AddChild(new Control
{
Children =
{
_smallActionIcon,
_smallItemSpriteView
}
});
AddChild(_bigActionIcon);
AddChild(_bigItemSpriteView);
AddChild(_cooldownGraphic);
AddChild(paddingBox);
AddChild(paddingBoxItemIcon);
DrawModeChanged();
}
private Control? SupplyTooltip(Control sender)
{
return Action == null ? null :
new ActionAlertTooltip(Action.Name, Action.Description, Action.Requires) {Cooldown = Cooldown};
}
/// <summary>
/// Action attempt for performing the action in the slot
/// </summary>
public IActionAttempt? ActionAttempt()
{
IActionAttempt? attempt = Action switch
{
ActionPrototype actionPrototype => new ActionAttempt(actionPrototype),
ItemActionPrototype itemActionPrototype =>
(Item != null && Item.TryGetComponent<ItemActionsComponent>(out var itemActions)) ?
new ItemActionAttempt(itemActionPrototype, Item, itemActions) : null,
_ => null
};
return attempt;
}
protected override void MouseEntered()
{
base.MouseEntered();
_beingHovered = true;
DrawModeChanged();
if (Action is not ItemActionPrototype) return;
if (Item == null) return;
_actionsComponent.HighlightItemSlot(Item);
}
protected override void MouseExited()
{
base.MouseExited();
_beingHovered = false;
CancelPress();
DrawModeChanged();
_actionsComponent.StopHighlightingItemSlots();
}
protected override void KeyBindDown(GUIBoundKeyEventArgs args)
{
base.KeyBindDown(args);
if (args.Function == EngineKeyFunctions.UIRightClick)
{
if (!_actionsUI.Locked && !_actionsUI.DragDropHelper.IsDragging)
{
_actionsComponent.Assignments.ClearSlot(_actionsUI.SelectedHotbar, SlotIndex, true);
_actionsUI.StopTargeting();
_actionsUI.UpdateUI();
}
return;
}
// only handle clicks, and can't do anything to this if no assignment
if (args.Function != EngineKeyFunctions.UIClick || !HasAssignment)
return;
// might turn into a drag or a full press if released
Depress(true);
_actionsUI.DragDropHelper.MouseDown(this);
DrawModeChanged();
}
protected override void KeyBindUp(GUIBoundKeyEventArgs args)
{
base.KeyBindUp(args);
if (args.Function != EngineKeyFunctions.UIClick)
return;
// might be finishing a drag or using the action
if (_actionsUI.DragDropHelper.IsDragging &&
_actionsUI.DragDropHelper.Dragged == this &&
UserInterfaceManager.CurrentlyHovered is ActionSlot targetSlot &&
targetSlot != this)
{
// finish the drag, swap the 2 slots
var fromIdx = SlotIndex;
var fromAssignment = _actionsComponent.Assignments[_actionsUI.SelectedHotbar, fromIdx];
var toIdx = targetSlot.SlotIndex;
var toAssignment = _actionsComponent.Assignments[_actionsUI.SelectedHotbar, toIdx];
if (fromIdx == toIdx) return;
if (!fromAssignment.HasValue) return;
_actionsComponent.Assignments.AssignSlot(_actionsUI.SelectedHotbar, toIdx, fromAssignment.Value);
if (toAssignment.HasValue)
{
_actionsComponent.Assignments.AssignSlot(_actionsUI.SelectedHotbar, fromIdx, toAssignment.Value);
}
else
{
_actionsComponent.Assignments.ClearSlot(_actionsUI.SelectedHotbar, fromIdx, false);
}
_actionsUI.UpdateUI();
}
else
{
// perform the action
if (UserInterfaceManager.CurrentlyHovered == this)
{
Depress(false);
}
}
_actionsUI.DragDropHelper.EndDrag();
DrawModeChanged();
}
/// <summary>
/// Cancel current press without triggering the action
/// </summary>
public void CancelPress()
{
_depressed = false;
DrawModeChanged();
}
/// <summary>
/// Press this button down. If it was depressed and now set to not depressed, will
/// trigger the action. Only has an effect if CanUseAction.
/// </summary>
public void Depress(bool depress)
{
if (!CanUseAction) return;
if (_depressed && !depress)
{
// fire the action
// no left-click interaction with it on cooldown or revoked
_actionsComponent.AttemptAction(this);
}
_depressed = depress;
DrawModeChanged();
}
/// <summary>
/// Updates the action assigned to this slot.
/// </summary>
/// <param name="action">action to assign</param>
/// <param name="actionEnabled">whether action should initially appear enable or disabled</param>
public void Assign(ActionPrototype action, bool actionEnabled)
{
// already assigned
if (Action != null && Action == action) return;
Action = action;
Item = null;
_depressed = false;
ToggledOn = false;
ActionEnabled = actionEnabled;
Cooldown = null;
HideTooltip();
UpdateIcons();
DrawModeChanged();
_number.SetMessage(SlotNumberLabel());
}
/// <summary>
/// Updates the item action assigned to this slot. The action will always be shown as disabled
/// until it is tied to a specific item.
/// </summary>
/// <param name="action">action to assign</param>
public void Assign(ItemActionPrototype action)
{
// already assigned
if (Action != null && Action == action && Item == null) return;
Action = action;
Item = null;
_depressed = false;
ToggledOn = false;
ActionEnabled = false;
Cooldown = null;
HideTooltip();
UpdateIcons();
DrawModeChanged();
_number.SetMessage(SlotNumberLabel());
}
/// <summary>
/// Updates the item action assigned to this slot, tied to a specific item.
/// </summary>
/// <param name="action">action to assign</param>
/// <param name="item">item the action is provided by</param>
/// <param name="actionEnabled">whether action should initially appear enable or disabled</param>
public void Assign(ItemActionPrototype action, IEntity item, bool actionEnabled)
{
// already assigned
if (Action != null && Action == action && Item == item) return;
Action = action;
Item = item;
_depressed = false;
ToggledOn = false;
ActionEnabled = false;
Cooldown = null;
HideTooltip();
UpdateIcons();
DrawModeChanged();
_number.SetMessage(SlotNumberLabel());
}
/// <summary>
/// Clears the action assigned to this slot
/// </summary>
public void Clear()
{
if (!HasAssignment) return;
Action = null;
Item = null;
ToggledOn = false;
_depressed = false;
Cooldown = null;
HideTooltip();
UpdateIcons();
DrawModeChanged();
_number.SetMessage(SlotNumberLabel());
}
/// <summary>
/// Display the action in this slot (if there is one) as enabled
/// </summary>
public void EnableAction()
{
if (ActionEnabled || !HasAssignment) return;
ActionEnabled = true;
_depressed = false;
DrawModeChanged();
_number.SetMessage(SlotNumberLabel());
}
/// <summary>
/// Display the action in this slot (if there is one) as disabled.
/// The slot is still clickable.
/// </summary>
public void DisableAction()
{
if (!ActionEnabled || !HasAssignment) return;
ActionEnabled = false;
_depressed = false;
DrawModeChanged();
_number.SetMessage(SlotNumberLabel());
}
private FormattedMessage SlotNumberLabel()
{
if (SlotNumber > 10) return FormattedMessage.FromMarkup("");
var number = Loc.GetString(SlotNumber == 10 ? "0" : SlotNumber.ToString());
var color = (ActionEnabled || !HasAssignment) ? EnabledColor : DisabledColor;
return FormattedMessage.FromMarkup("[color=" + color + "]" + number + "[/color]");
}
private void UpdateIcons()
{
if (!HasAssignment)
{
SetActionIcon(null);
SetItemIcon(null);
return;
}
if (HasToggleSprite && ToggledOn && Action != null)
{
SetActionIcon(Action.IconOn.Frame0());
}
else if (Action != null)
{
SetActionIcon(Action.Icon.Frame0());
}
if (Item != null)
{
SetItemIcon(Item.TryGetComponent<ISpriteComponent>(out var spriteComponent) ? spriteComponent : null);
}
else
{
SetItemIcon(null);
}
}
private void SetActionIcon(Texture? texture)
{
if (texture == null || !HasAssignment)
{
_bigActionIcon.Texture = null;
_bigActionIcon.Visible = false;
_smallActionIcon.Texture = null;
_smallActionIcon.Visible = false;
}
else
{
if (Action is ItemActionPrototype {IconStyle: ItemActionIconStyle.BigItem})
{
_bigActionIcon.Texture = null;
_bigActionIcon.Visible = false;
_smallActionIcon.Texture = texture;
_smallActionIcon.Visible = true;
}
else
{
_bigActionIcon.Texture = texture;
_bigActionIcon.Visible = true;
_smallActionIcon.Texture = null;
_smallActionIcon.Visible = false;
}
}
}
private void SetItemIcon(ISpriteComponent? sprite)
{
if (sprite == null || !HasAssignment)
{
_bigItemSpriteView.Visible = false;
_bigItemSpriteView.Sprite = null;
_smallItemSpriteView.Visible = false;
_smallItemSpriteView.Sprite = null;
}
else
{
if (Action is ItemActionPrototype actionPrototype)
{
switch (actionPrototype.IconStyle)
{
case ItemActionIconStyle.BigItem:
{
_bigItemSpriteView.Visible = true;
_bigItemSpriteView.Sprite = sprite;
_smallItemSpriteView.Visible = false;
_smallItemSpriteView.Sprite = null;
break;
}
case ItemActionIconStyle.BigAction:
{
_bigItemSpriteView.Visible = false;
_bigItemSpriteView.Sprite = null;
_smallItemSpriteView.Visible = true;
_smallItemSpriteView.Sprite = sprite;
break;
}
case ItemActionIconStyle.NoItem:
{
_bigItemSpriteView.Visible = false;
_bigItemSpriteView.Sprite = null;
_smallItemSpriteView.Visible = false;
_smallItemSpriteView.Sprite = null;
break;
}
}
}
else
{
_bigItemSpriteView.Visible = false;
_bigItemSpriteView.Sprite = null;
_smallItemSpriteView.Visible = false;
_smallItemSpriteView.Sprite = null;
}
}
}
private void DrawModeChanged()
{
// always show the normal empty button style if no action in this slot
if (!HasAssignment)
{
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassNormal);
return;
}
// it's only depress-able if it's usable, so if we're depressed
// show the depressed style
if (_depressed)
{
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassPressed);
return;
}
// show a hover only if the action is usable
if (_beingHovered)
{
if (ActionEnabled && !IsOnCooldown)
{
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassHover);
return;
}
}
// if it's toggled on, always show the toggled on style (currently same as depressed style)
if (ToggledOn)
{
// when there's a toggle sprite, we're showing that sprite instead of highlighting this slot
SetOnlyStylePseudoClass(HasToggleSprite ? ContainerButton.StylePseudoClassNormal :
ContainerButton.StylePseudoClassPressed);
return;
}
if (!ActionEnabled)
{
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassDisabled);
return;
}
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassNormal);
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
if (!Cooldown.HasValue)
{
_cooldownGraphic.Visible = false;
_cooldownGraphic.Progress = 0;
return;
}
var duration = Cooldown.Value.End - Cooldown.Value.Start;
var curTime = _gameTiming.CurTime;
var length = duration.TotalSeconds;
var progress = (curTime - Cooldown.Value.Start).TotalSeconds / length;
var ratio = (progress <= 1 ? (1 - progress) : (curTime - Cooldown.Value.End).TotalSeconds * -5);
_cooldownGraphic.Progress = MathHelper.Clamp((float)ratio, -1, 1);
_cooldownGraphic.Visible = ratio > -1f;
}
}
}

View File

@@ -0,0 +1,110 @@
#nullable enable
using System;
using Content.Client.Utility;
using Content.Shared.Alert;
using Robust.Client.Interfaces.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Interfaces.Timing;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
namespace Content.Client.UserInterface.Controls
{
public class AlertControl : BaseButton
{
// shorter than default tooltip delay so user can more easily
// see what alerts they have
private const float CustomTooltipDelay = 0.5f;
public AlertPrototype Alert { get; }
/// <summary>
/// Current cooldown displayed in this slot. Set to null to show no cooldown.
/// </summary>
public (TimeSpan Start, TimeSpan End)? Cooldown
{
get => _cooldown;
set
{
_cooldown = value;
if (SuppliedTooltip is ActionAlertTooltip actionAlertTooltip)
{
actionAlertTooltip.Cooldown = value;
}
}
}
private (TimeSpan Start, TimeSpan End)? _cooldown;
private short? _severity;
private readonly IGameTiming _gameTiming;
private readonly TextureRect _icon;
private readonly CooldownGraphic _cooldownGraphic;
private readonly IResourceCache _resourceCache;
/// <summary>
/// Creates an alert control reflecting the indicated alert + state
/// </summary>
/// <param name="alert">alert to display</param>
/// <param name="severity">severity of alert, null if alert doesn't have severity levels</param>
/// <param name="resourceCache">resourceCache to use to load alert icon textures</param>
public AlertControl(AlertPrototype alert, short? severity, IResourceCache resourceCache)
{
_gameTiming = IoCManager.Resolve<IGameTiming>();
TooltipDelay = CustomTooltipDelay;
TooltipSupplier = SupplyTooltip;
_resourceCache = resourceCache;
Alert = alert;
_severity = severity;
var texture = _resourceCache.GetTexture(alert.GetIconPath(_severity));
_icon = new TextureRect
{
TextureScale = (2, 2),
Texture = texture
};
Children.Add(_icon);
_cooldownGraphic = new CooldownGraphic();
Children.Add(_cooldownGraphic);
}
private Control SupplyTooltip(Control? sender)
{
return new ActionAlertTooltip(Alert.Name, Alert.Description) {Cooldown = Cooldown};
}
/// <summary>
/// Change the alert severity, changing the displayed icon
/// </summary>
public void SetSeverity(short? severity)
{
if (_severity != severity)
{
_severity = severity;
_icon.Texture = _resourceCache.GetTexture(Alert.GetIconPath(_severity));
}
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
if (!Cooldown.HasValue)
{
_cooldownGraphic.Visible = false;
_cooldownGraphic.Progress = 0;
return;
}
var duration = Cooldown.Value.End - Cooldown.Value.Start;
var curTime = _gameTiming.CurTime;
var length = duration.TotalSeconds;
var progress = (curTime - Cooldown.Value.Start).TotalSeconds / length;
var ratio = (progress <= 1 ? (1 - progress) : (curTime - Cooldown.Value.End).TotalSeconds * -5);
_cooldownGraphic.Progress = MathHelper.Clamp((float)ratio, -1, 1);
_cooldownGraphic.Visible = ratio > -1f;
}
}
}

View File

@@ -1,14 +1,19 @@
using System;
using Robust.Client.Graphics;
using Robust.Client.Graphics.Shaders;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Input;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
namespace Content.Client.UserInterface
{
public class ItemSlotButton : MarginContainer
{
private const string HighlightShader = "SelectionOutlineInrange";
public TextureRect Button { get; }
public SpriteView SpriteView { get; }
public SpriteView HoverSpriteView { get; }
@@ -21,9 +26,11 @@ namespace Content.Client.UserInterface
public bool EntityHover => HoverSpriteView.Sprite != null;
public bool MouseIsHovering = false;
private readonly ShaderInstance _highlightShader;
public ItemSlotButton(Texture texture, Texture storageTexture)
{
_highlightShader = IoCManager.Resolve<IPrototypeManager>().Index<ShaderPrototype>(HighlightShader).Instance();
CustomMinimumSize = (64, 64);
AddChild(Button = new TextureRect
@@ -95,6 +102,20 @@ namespace Content.Client.UserInterface
}
}
public void Highlight(bool on)
{
// I make no claim that this actually looks good but it's a start.
if (on)
{
Button.ShaderOverride = _highlightShader;
}
else
{
Button.ShaderOverride = null;
}
}
private void OnButtonPressed(GUIBoundKeyEventArgs args)
{
OnPressed?.Invoke(args);

View File

@@ -150,7 +150,6 @@ namespace Content.Client.UserInterface
AddButton(ContentKeyFunctions.ReleasePulledObject, "Release pulled object");
AddButton(ContentKeyFunctions.Point, "Point at location");
AddHeader("User Interface");
AddButton(ContentKeyFunctions.FocusChat, "Focus chat");
AddButton(ContentKeyFunctions.FocusOOC, "Focus chat (OOC)");
@@ -160,6 +159,7 @@ namespace Content.Client.UserInterface
AddButton(ContentKeyFunctions.OpenCraftingMenu, "Open crafting menu");
AddButton(ContentKeyFunctions.OpenInventoryMenu, "Open inventory");
AddButton(ContentKeyFunctions.OpenTutorial, "Open tutorial");
AddButton(ContentKeyFunctions.OpenActionsMenu, "Open action menu");
AddButton(ContentKeyFunctions.OpenEntitySpawnWindow, "Open entity spawn menu");
AddButton(ContentKeyFunctions.OpenSandboxWindow, "Open sandbox menu");
AddButton(ContentKeyFunctions.OpenTileSpawnWindow, "Open tile spawn menu");
@@ -169,6 +169,18 @@ namespace Content.Client.UserInterface
AddButton(ContentKeyFunctions.TakeScreenshot, "Take screenshot");
AddButton(ContentKeyFunctions.TakeScreenshotNoUI, "Take screenshot (without UI)");
AddHeader("Hotbar");
AddButton(ContentKeyFunctions.Hotbar1, "Hotbar slot 1");
AddButton(ContentKeyFunctions.Hotbar2, "Hotbar slot 2");
AddButton(ContentKeyFunctions.Hotbar3, "Hotbar slot 3");
AddButton(ContentKeyFunctions.Hotbar4, "Hotbar slot 4");
AddButton(ContentKeyFunctions.Hotbar5, "Hotbar slot 5");
AddButton(ContentKeyFunctions.Hotbar6, "Hotbar slot 6");
AddButton(ContentKeyFunctions.Hotbar7, "Hotbar slot 7");
AddButton(ContentKeyFunctions.Hotbar8, "Hotbar slot 8");
AddButton(ContentKeyFunctions.Hotbar9, "Hotbar slot 9");
AddButton(ContentKeyFunctions.Hotbar0, "Hotbar slot 0");
AddHeader("Map Editor");
AddButton(EngineKeyFunctions.EditorPlaceObject, "Place object");
AddButton(EngineKeyFunctions.EditorCancelPlace, "Cancel placement");

View File

@@ -1,5 +1,6 @@
using System.Linq;
using Content.Client.GameObjects.EntitySystems;
using Content.Client.UserInterface.Controls;
using Content.Client.Utility;
using Robust.Client.Graphics.Drawing;
using Robust.Client.Interfaces.ResourceManagement;
@@ -15,10 +16,19 @@ namespace Content.Client.UserInterface.Stylesheets
{
public const string StyleClassBorderedWindowPanel = "BorderedWindowPanel";
public const string StyleClassTransparentBorderedWindowPanel = "TransparentBorderedWindowPanel";
public const string StyleClassHotbarPanel = "HotbarPanel";
public const string StyleClassTooltipPanel = "tooltipBox";
public const string StyleClassTooltipAlertTitle = "tooltipAlertTitle";
public const string StyleClassTooltipAlertDescription = "tooltipAlertDesc";
public const string StyleClassTooltipAlertCooldown = "tooltipAlertCooldown";
public const string StyleClassTooltipActionTitle = "tooltipActionTitle";
public const string StyleClassTooltipActionDescription = "tooltipActionDesc";
public const string StyleClassTooltipActionCooldown = "tooltipActionCooldown";
public const string StyleClassTooltipActionRequirements = "tooltipActionCooldown";
public const string StyleClassHotbarSlotNumber = "hotbarSlotNumber";
public const string StyleClassActionSearchBox = "actionSearchBox";
public const string StyleClassActionMenuItemRevoked = "actionMenuItemRevoked";
public const string StyleClassSliderRed = "Red";
public const string StyleClassSliderGreen = "Green";
@@ -60,6 +70,8 @@ namespace Content.Client.UserInterface.Stylesheets
var notoSansItalic12 = resCache.GetFont("/Fonts/NotoSans/NotoSans-Italic.ttf", 12);
var notoSansBold12 = resCache.GetFont("/Fonts/NotoSans/NotoSans-Bold.ttf", 12);
var notoSansDisplayBold14 = resCache.GetFont("/Fonts/NotoSansDisplay/NotoSansDisplay-Bold.ttf", 14);
var notoSansDisplayBold16 = resCache.GetFont("/Fonts/NotoSansDisplay/NotoSansDisplay-Bold.ttf", 16);
var notoSans15 = resCache.GetFont("/Fonts/NotoSans/NotoSans-Regular.ttf", 15);
var notoSans16 = resCache.GetFont("/Fonts/NotoSans/NotoSans-Regular.ttf", 16);
var notoSansBold16 = resCache.GetFont("/Fonts/NotoSans/NotoSans-Bold.ttf", 16);
var notoSansBold18 = resCache.GetFont("/Fonts/NotoSans/NotoSans-Bold.ttf", 18);
@@ -95,6 +107,61 @@ namespace Content.Client.UserInterface.Stylesheets
};
borderedTransparentWindowBackground.SetPatchMargin(StyleBox.Margin.All, 2);
var hotbarBackground = new StyleBoxTexture
{
Texture = borderedWindowBackgroundTex,
};
hotbarBackground.SetPatchMargin(StyleBox.Margin.All, 2);
hotbarBackground.SetExpandMargin(StyleBox.Margin.All, 4);
var buttonRectTex = resCache.GetTexture("/Textures/Interface/Nano/light_panel_background_bordered.png");
var buttonRect = new StyleBoxTexture(BaseButton)
{
Texture = buttonRectTex
};
buttonRect.SetPatchMargin(StyleBox.Margin.All, 2);
buttonRect.SetPadding(StyleBox.Margin.All, 2);
buttonRect.SetContentMarginOverride(StyleBox.Margin.Vertical, 2);
buttonRect.SetContentMarginOverride(StyleBox.Margin.Horizontal, 2);
var buttonRectHover = new StyleBoxTexture(buttonRect)
{
Modulate = ButtonColorHovered
};
var buttonRectPressed = new StyleBoxTexture(buttonRect)
{
Modulate = ButtonColorPressed
};
var buttonRectDisabled = new StyleBoxTexture(buttonRect)
{
Modulate = ButtonColorDisabled
};
var buttonRectActionMenuItemTex = resCache.GetTexture("/Textures/Interface/Nano/black_panel_light_thin_border.png");
var buttonRectActionMenuRevokedItemTex = resCache.GetTexture("/Textures/Interface/Nano/black_panel_red_thin_border.png");
var buttonRectActionMenuItem = new StyleBoxTexture(BaseButton)
{
Texture = buttonRectActionMenuItemTex
};
buttonRectActionMenuItem.SetPatchMargin(StyleBox.Margin.All, 2);
buttonRectActionMenuItem.SetPadding(StyleBox.Margin.All, 2);
buttonRectActionMenuItem.SetContentMarginOverride(StyleBox.Margin.Vertical, 2);
buttonRectActionMenuItem.SetContentMarginOverride(StyleBox.Margin.Horizontal, 2);
var buttonRectActionMenuItemRevoked = new StyleBoxTexture(buttonRectActionMenuItem)
{
Texture = buttonRectActionMenuRevokedItemTex
};
var buttonRectActionMenuItemHover = new StyleBoxTexture(buttonRectActionMenuItem)
{
Modulate = ButtonColorHovered
};
var buttonRectActionMenuItemPressed = new StyleBoxTexture(buttonRectActionMenuItem)
{
Modulate = ButtonColorPressed
};
var textureInvertedTriangle = resCache.GetTexture("/Textures/Interface/Nano/inverted_triangle.svg.png");
var lineEditTex = resCache.GetTexture("/Textures/Interface/Nano/lineedit.png");
@@ -105,6 +172,14 @@ namespace Content.Client.UserInterface.Stylesheets
lineEdit.SetPatchMargin(StyleBox.Margin.All, 3);
lineEdit.SetContentMarginOverride(StyleBox.Margin.Horizontal, 5);
var actionSearchBoxTex = resCache.GetTexture("/Textures/Interface/Nano/black_panel_dark_thin_border.png");
var actionSearchBox = new StyleBoxTexture
{
Texture = actionSearchBoxTex,
};
actionSearchBox.SetPatchMargin(StyleBox.Margin.All, 3);
actionSearchBox.SetContentMarginOverride(StyleBox.Margin.Horizontal, 5);
var tabContainerPanelTex = resCache.GetTexture("/Textures/Interface/Nano/tabcontainer_panel.png");
var tabContainerPanel = new StyleBoxTexture
{
@@ -280,6 +355,12 @@ namespace Content.Client.UserInterface.Stylesheets
{
new StyleProperty(PanelContainer.StylePropertyPanel, borderedTransparentWindowBackground),
}),
// Hotbar background
new StyleRule(new SelectorElement(typeof(PanelContainer), new[] {StyleClassHotbarPanel}, null, null),
new[]
{
new StyleProperty(PanelContainer.StylePropertyPanel, hotbarBackground),
}),
// Window header.
new StyleRule(
new SelectorElement(typeof(PanelContainer), new[] {SS14Window.StyleClassWindowHeader}, null, null),
@@ -376,6 +457,43 @@ namespace Content.Client.UserInterface.Stylesheets
new StyleProperty("font-color", Color.FromHex("#E5E5E581")),
}),
// action slot hotbar buttons
new StyleRule(new SelectorElement(typeof(ActionSlot), null, null, new[] {ContainerButton.StylePseudoClassNormal}), new[]
{
new StyleProperty(PanelContainer.StylePropertyPanel, buttonRect),
}),
new StyleRule(new SelectorElement(typeof(ActionSlot), null, null, new[] {ContainerButton.StylePseudoClassHover}), new[]
{
new StyleProperty(PanelContainer.StylePropertyPanel, buttonRectHover),
}),
new StyleRule(new SelectorElement(typeof(ActionSlot), null, null, new[] {ContainerButton.StylePseudoClassPressed}), new[]
{
new StyleProperty(PanelContainer.StylePropertyPanel, buttonRectPressed),
}),
new StyleRule(new SelectorElement(typeof(ActionSlot), null, null, new[] {ContainerButton.StylePseudoClassDisabled}), new[]
{
new StyleProperty(PanelContainer.StylePropertyPanel, buttonRectDisabled),
}),
// action menu item buttons
new StyleRule(new SelectorElement(typeof(ActionMenuItem), null, null, new[] {ContainerButton.StylePseudoClassNormal}), new[]
{
new StyleProperty(ContainerButton.StylePropertyStyleBox, buttonRectActionMenuItem),
}),
// we don't actually disable the action menu items, only change their style based on the underlying action being revoked
new StyleRule(new SelectorElement(typeof(ActionMenuItem), new [] {StyleClassActionMenuItemRevoked}, null, new[] {ContainerButton.StylePseudoClassNormal}), new[]
{
new StyleProperty(ContainerButton.StylePropertyStyleBox, buttonRectActionMenuItemRevoked),
}),
new StyleRule(new SelectorElement(typeof(ActionMenuItem), null, null, new[] {ContainerButton.StylePseudoClassHover}), new[]
{
new StyleProperty(ContainerButton.StylePropertyStyleBox, buttonRectActionMenuItemHover),
}),
new StyleRule(new SelectorElement(typeof(ActionMenuItem), null, null, new[] {ContainerButton.StylePseudoClassPressed}), new[]
{
new StyleProperty(ContainerButton.StylePropertyStyleBox, buttonRectActionMenuItemPressed),
}),
// Main menu: Make those buttons bigger.
new StyleRule(new SelectorChild(
new SelectorElement(typeof(Button), null, "mainMenu", null),
@@ -413,6 +531,13 @@ namespace Content.Client.UserInterface.Stylesheets
new StyleProperty("font-color", Color.Gray),
}),
// Action searchbox lineedit
new StyleRule(new SelectorElement(typeof(LineEdit), new[] {StyleClassActionSearchBox}, null, null),
new[]
{
new StyleProperty(LineEdit.StylePropertyStyleBox, actionSearchBox),
}),
// TabContainer
new StyleRule(new SelectorElement(typeof(TabContainer), null, null, null),
new[]
@@ -531,6 +656,30 @@ namespace Content.Client.UserInterface.Stylesheets
new StyleProperty("font", notoSans16)
}),
// action tooltip
new StyleRule(new SelectorElement(typeof(RichTextLabel), new[] {StyleClassTooltipActionTitle}, null, null), new[]
{
new StyleProperty("font", notoSansBold16)
}),
new StyleRule(new SelectorElement(typeof(RichTextLabel), new[] {StyleClassTooltipActionDescription}, null, null), new[]
{
new StyleProperty("font", notoSans15)
}),
new StyleRule(new SelectorElement(typeof(RichTextLabel), new[] {StyleClassTooltipActionCooldown}, null, null), new[]
{
new StyleProperty("font", notoSans15)
}),
new StyleRule(new SelectorElement(typeof(RichTextLabel), new[] {StyleClassTooltipActionRequirements}, null, null), new[]
{
new StyleProperty("font", notoSans15)
}),
// hotbar slot
new StyleRule(new SelectorElement(typeof(RichTextLabel), new[] {StyleClassHotbarSlotNumber}, null, null), new[]
{
new StyleProperty("font", notoSansDisplayBold16)
}),
// Entity tooltip
new StyleRule(
new SelectorElement(typeof(PanelContainer), new[] {ExamineSystem.StyleClassEntityTooltip}, null,

View File

@@ -74,6 +74,7 @@ Smart equip from belt: [color=#a4885c]{25}[/color]
Open inventory: [color=#a4885c]{7}[/color]
Open character window: [color=#a4885c]{8}[/color]
Open crafting window: [color=#a4885c]{9}[/color]
Open action menu: [color=#a4885c]{33}[/color]
Focus chat: [color=#a4885c]{10}[/color]
Focus OOC: [color=#a4885c]{26}[/color]
Focus Admin Chat: [color=#a4885c]{27}[/color]
@@ -94,7 +95,18 @@ Toggle debug overlay: [color=#a4885c]{18}[/color]
Toggle entity spawner: [color=#a4885c]{19}[/color]
Toggle tile spawner: [color=#a4885c]{20}[/color]
Toggle sandbox window: [color=#a4885c]{21}[/color]
Toggle admin menu [color=#a4885c]{31}[/color]",
Toggle admin menu [color=#a4885c]{31}[/color]
Hotbar slot 1: [color=#a4885c]{34}[/color]
Hotbar slot 2: [color=#a4885c]{35}[/color]
Hotbar slot 3: [color=#a4885c]{36}[/color]
Hotbar slot 4: [color=#a4885c]{37}[/color]
Hotbar slot 5: [color=#a4885c]{38}[/color]
Hotbar slot 6: [color=#a4885c]{39}[/color]
Hotbar slot 7: [color=#a4885c]{40}[/color]
Hotbar slot 8: [color=#a4885c]{41}[/color]
Hotbar slot 9: [color=#a4885c]{42}[/color]
Hotbar slot 0: [color=#a4885c]{43}[/color]
",
Key(MoveUp), Key(MoveLeft), Key(MoveDown), Key(MoveRight),
Key(SwapHands),
Key(ActivateItemInHand),
@@ -124,7 +136,18 @@ Toggle admin menu [color=#a4885c]{31}[/color]",
Key(TryPullObject),
Key(MovePulledObject),
Key(OpenAdminMenu),
Key(ReleasePulledObject)));
Key(ReleasePulledObject),
Key(OpenActionsMenu),
Key(Hotbar1),
Key(Hotbar2),
Key(Hotbar3),
Key(Hotbar4),
Key(Hotbar5),
Key(Hotbar6),
Key(Hotbar7),
Key(Hotbar8),
Key(Hotbar9),
Key(Hotbar0)));
//Gameplay
VBox.AddChild(new Label { FontOverride = headerFont, Text = "\nGameplay" });