Re-organize all projects (#4166)
This commit is contained in:
96
Content.Client/Actions/UI/ActionAlertTooltip.cs
Normal file
96
Content.Client/Actions/UI/ActionAlertTooltip.cs
Normal file
@@ -0,0 +1,96 @@
|
||||
using System;
|
||||
using Content.Client.Stylesheets;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Actions.UI
|
||||
{
|
||||
/// <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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
517
Content.Client/Actions/UI/ActionMenu.cs
Normal file
517
Content.Client/Actions/UI/ActionMenu.cs
Normal file
@@ -0,0 +1,517 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Content.Client.Actions.Assignments;
|
||||
using Content.Client.DragDrop;
|
||||
using Content.Client.HUD;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Actions.Prototypes;
|
||||
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;
|
||||
using static Robust.Client.UserInterface.Controls.BaseButton;
|
||||
|
||||
namespace Content.Client.Actions.UI
|
||||
{
|
||||
/// <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>();
|
||||
|
||||
/// <summary>
|
||||
/// Is an action currently being dragged from this window?
|
||||
/// </summary>
|
||||
public bool IsDragging => _dragDropHelper.IsDragging;
|
||||
|
||||
// parallel list of actions currently selectable in itemList
|
||||
private BaseActionPrototype[] _actionList = new BaseActionPrototype[0];
|
||||
|
||||
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 IGameHud _gameHud;
|
||||
private readonly DragDropHelper<ActionMenuItem> _dragDropHelper;
|
||||
|
||||
|
||||
public ActionMenu(ClientActionsComponent actionsComponent, ActionsUI actionsUI)
|
||||
{
|
||||
_actionsComponent = actionsComponent;
|
||||
_actionsUI = actionsUI;
|
||||
_actionManager = IoCManager.Resolve<ActionManager>();
|
||||
_gameHud = IoCManager.Resolve<IGameHud>();
|
||||
|
||||
Title = Loc.GetString("Actions");
|
||||
MinSize = (300, 300);
|
||||
|
||||
Contents.AddChild(new VBoxContainer
|
||||
{
|
||||
Children =
|
||||
{
|
||||
new HBoxContainer
|
||||
{
|
||||
Children =
|
||||
{
|
||||
(_searchBar = new LineEdit
|
||||
{
|
||||
StyleClasses = { StyleNano.StyleClassActionSearchBox },
|
||||
HorizontalExpand = true,
|
||||
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? MinSize = new Vector2(200.0f, 0.0f),
|
||||
VerticalExpand = true,
|
||||
HorizontalExpand = true,
|
||||
Children =
|
||||
{
|
||||
(_resultsGrid = new GridContainer
|
||||
{
|
||||
MaxGridWidth = 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
|
||||
{
|
||||
MinSize = (64, 64),
|
||||
Stretch = TextureRect.StretchMode.Scale,
|
||||
Visible = false,
|
||||
SetSize = (64, 64)
|
||||
};
|
||||
UserInterfaceManager.PopupRoot.AddChild(_dragShadow);
|
||||
|
||||
_dragDropHelper = new DragDropHelper<ActionMenuItem>(OnBeginActionDrag, OnContinueActionDrag, OnEndActionDrag);
|
||||
}
|
||||
|
||||
protected override void EnteredTree()
|
||||
{
|
||||
base.EnteredTree();
|
||||
_clearButton.OnPressed += OnClearButtonPressed;
|
||||
_searchBar.OnTextChanged += OnSearchTextChanged;
|
||||
_filterButton.OnItemSelected += OnFilterItemSelected;
|
||||
_gameHud.ActionsButtonDown = true;
|
||||
foreach (var actionMenuControl in _resultsGrid.Children)
|
||||
{
|
||||
var actionMenuItem = (ActionMenuItem) actionMenuControl;
|
||||
actionMenuItem.OnButtonDown += OnItemButtonDown;
|
||||
actionMenuItem.OnButtonUp += OnItemButtonUp;
|
||||
actionMenuItem.OnPressed += OnItemPressed;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ExitedTree()
|
||||
{
|
||||
base.ExitedTree();
|
||||
_dragDropHelper.EndDrag();
|
||||
_clearButton.OnPressed -= OnClearButtonPressed;
|
||||
_searchBar.OnTextChanged -= OnSearchTextChanged;
|
||||
_filterButton.OnItemSelected -= OnFilterItemSelected;
|
||||
_gameHud.ActionsButtonDown = false;
|
||||
foreach (var actionMenuControl in _resultsGrid.Children)
|
||||
{
|
||||
var actionMenuItem = (ActionMenuItem) actionMenuControl;
|
||||
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.MaxGridWidth = 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.Position - (32, 32));
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool OnContinueActionDrag(float frameTime)
|
||||
{
|
||||
// keep dragged entity centered under mouse
|
||||
LayoutContainer.SetPosition(_dragShadow, UserInterfaceManager.MousePositionScaled.Position - (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(ButtonEventArgs args)
|
||||
{
|
||||
if (args.Event.Function != EngineKeyFunctions.UIClick ||
|
||||
args.Button is not ActionMenuItem action)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_dragDropHelper.MouseDown(action);
|
||||
}
|
||||
|
||||
private void OnItemButtonUp(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 OnItemFocusExited(ActionMenuItem item)
|
||||
{
|
||||
// lost focus, cancel the drag if one is in progress
|
||||
_dragDropHelper.EndDrag();
|
||||
}
|
||||
|
||||
private void OnItemPressed(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(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.IsTargetAction,
|
||||
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, OnItemFocusExited);
|
||||
_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.FrameUpdate(args);
|
||||
_dragDropHelper.Update(args.DeltaSeconds);
|
||||
}
|
||||
}
|
||||
}
|
||||
76
Content.Client/Actions/UI/ActionMenuItem.cs
Normal file
76
Content.Client/Actions/UI/ActionMenuItem.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
using System;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Shared.Actions.Prototypes;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.Utility;
|
||||
|
||||
namespace Content.Client.Actions.UI
|
||||
{
|
||||
/// <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; }
|
||||
|
||||
private Action<ActionMenuItem> _onControlFocusExited;
|
||||
|
||||
public ActionMenuItem(BaseActionPrototype action, Action<ActionMenuItem> onControlFocusExited)
|
||||
{
|
||||
_onControlFocusExited = onControlFocusExited;
|
||||
Action = action;
|
||||
|
||||
MinSize = (64, 64);
|
||||
VerticalAlignment = VAlignment.Top;
|
||||
|
||||
AddChild(new TextureRect
|
||||
{
|
||||
HorizontalExpand = true,
|
||||
VerticalExpand = true,
|
||||
Stretch = TextureRect.StretchMode.Scale,
|
||||
Texture = action.Icon.Frame0()
|
||||
});
|
||||
|
||||
TooltipDelay = CustomTooltipDelay;
|
||||
TooltipSupplier = SupplyTooltip;
|
||||
}
|
||||
|
||||
protected override void ControlFocusExited()
|
||||
{
|
||||
base.ControlFocusExited();
|
||||
_onControlFocusExited.Invoke(this);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
667
Content.Client/Actions/UI/ActionSlot.cs
Normal file
667
Content.Client/Actions/UI/ActionSlot.cs
Normal file
@@ -0,0 +1,667 @@
|
||||
using System;
|
||||
using Content.Client.Cooldown;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Actions.Components;
|
||||
using Content.Shared.Actions.Prototypes;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Actions.UI
|
||||
{
|
||||
/// <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?
|
||||
/// Target-basedActions on cooldown can still be selected / deselected if they've been configured as such
|
||||
/// </summary>
|
||||
public bool CanUseAction => Action != null && ActionEnabled &&
|
||||
(!IsOnCooldown || (Action.IsTargetAction && !Action.DeselectOnCooldown));
|
||||
|
||||
/// <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 ActionMenu _actionMenu;
|
||||
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, ActionMenu actionMenu, ClientActionsComponent actionsComponent, byte slotIndex)
|
||||
{
|
||||
_actionsComponent = actionsComponent;
|
||||
_actionsUI = actionsUI;
|
||||
_actionMenu = actionMenu;
|
||||
_gameTiming = IoCManager.Resolve<IGameTiming>();
|
||||
SlotIndex = slotIndex;
|
||||
MouseFilter = MouseFilterMode.Stop;
|
||||
|
||||
MinSize = (64, 64);
|
||||
VerticalAlignment = VAlignment.Top;
|
||||
TooltipDelay = CustomTooltipDelay;
|
||||
TooltipSupplier = SupplyTooltip;
|
||||
|
||||
_number = new RichTextLabel
|
||||
{
|
||||
StyleClasses = {StyleNano.StyleClassHotbarSlotNumber}
|
||||
};
|
||||
_number.SetMessage(SlotNumberLabel());
|
||||
|
||||
_bigActionIcon = new TextureRect
|
||||
{
|
||||
HorizontalExpand = true,
|
||||
VerticalExpand = true,
|
||||
Stretch = TextureRect.StretchMode.Scale,
|
||||
Visible = false
|
||||
};
|
||||
_bigItemSpriteView = new SpriteView
|
||||
{
|
||||
HorizontalExpand = true,
|
||||
VerticalExpand = true,
|
||||
Scale = (2,2),
|
||||
Visible = false,
|
||||
OverrideDirection = Direction.South,
|
||||
};
|
||||
_smallActionIcon = new TextureRect
|
||||
{
|
||||
HorizontalAlignment = HAlignment.Right,
|
||||
VerticalAlignment = VAlignment.Bottom,
|
||||
Stretch = TextureRect.StretchMode.Scale,
|
||||
Visible = false
|
||||
};
|
||||
_smallItemSpriteView = new SpriteView
|
||||
{
|
||||
HorizontalAlignment = HAlignment.Right,
|
||||
VerticalAlignment = VAlignment.Bottom,
|
||||
Visible = false
|
||||
};
|
||||
|
||||
_cooldownGraphic = new CooldownGraphic {Progress = 0, Visible = false};
|
||||
|
||||
// padding to the left of the number to shift it right
|
||||
var paddingBox = new HBoxContainer()
|
||||
{
|
||||
HorizontalExpand = true,
|
||||
VerticalExpand = true,
|
||||
MinSize = (64, 64)
|
||||
};
|
||||
paddingBox.AddChild(new Control()
|
||||
{
|
||||
MinSize = (4, 4),
|
||||
});
|
||||
paddingBox.AddChild(_number);
|
||||
|
||||
// padding to the left of the small icon
|
||||
var paddingBoxItemIcon = new HBoxContainer()
|
||||
{
|
||||
HorizontalExpand = true,
|
||||
VerticalExpand = true,
|
||||
MinSize = (64, 64)
|
||||
};
|
||||
paddingBoxItemIcon.AddChild(new Control()
|
||||
{
|
||||
MinSize = (32, 32),
|
||||
});
|
||||
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 && !_actionMenu.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();
|
||||
}
|
||||
|
||||
protected override void ControlFocusExited()
|
||||
{
|
||||
// lost focus for some reason, cancel the drag if there is one.
|
||||
base.ControlFocusExited();
|
||||
_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)
|
||||
{
|
||||
// action can still be toggled if it's allowed to stay selected
|
||||
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()
|
||||
{
|
||||
|
||||
// show a hover only if the action is usable or another action is being dragged on top of this
|
||||
if (_beingHovered)
|
||||
{
|
||||
if (_actionsUI.DragDropHelper.IsDragging || _actionMenu.IsDragging ||
|
||||
(HasAssignment && ActionEnabled && !IsOnCooldown))
|
||||
{
|
||||
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassHover);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
584
Content.Client/Actions/UI/ActionsUI.cs
Normal file
584
Content.Client/Actions/UI/ActionsUI.cs
Normal file
@@ -0,0 +1,584 @@
|
||||
using System.Collections.Generic;
|
||||
using Content.Client.Actions.Assignments;
|
||||
using Content.Client.DragDrop;
|
||||
using Content.Client.HUD;
|
||||
using Content.Client.Resources;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Actions.Prototypes;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.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.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.Actions.UI
|
||||
{
|
||||
/// <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 IGameHud _gameHud;
|
||||
|
||||
private readonly ActionSlot[] _slots;
|
||||
|
||||
private readonly GridContainer _slotContainer;
|
||||
|
||||
private readonly TextureButton _lockButton;
|
||||
private readonly TextureButton _settingsButton;
|
||||
private readonly Label _loadoutNumber;
|
||||
private readonly Texture _lockTexture;
|
||||
private readonly Texture _unlockTexture;
|
||||
private readonly HBoxContainer _loadoutContainer;
|
||||
|
||||
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)
|
||||
{
|
||||
SetValue(LayoutContainer.DebugProperty, true);
|
||||
_actionsComponent = actionsComponent;
|
||||
_actionManager = IoCManager.Resolve<ActionManager>();
|
||||
_entityManager = IoCManager.Resolve<IEntityManager>();
|
||||
_gameTiming = IoCManager.Resolve<IGameTiming>();
|
||||
_gameHud = IoCManager.Resolve<IGameHud>();
|
||||
_menu = new ActionMenu(_actionsComponent, this);
|
||||
|
||||
LayoutContainer.SetGrowHorizontal(this, LayoutContainer.GrowDirection.End);
|
||||
LayoutContainer.SetGrowVertical(this, LayoutContainer.GrowDirection.Constrain);
|
||||
LayoutContainer.SetAnchorTop(this, 0f);
|
||||
LayoutContainer.SetAnchorBottom(this, 0.8f);
|
||||
LayoutContainer.SetMarginLeft(this, 13);
|
||||
LayoutContainer.SetMarginTop(this, 110);
|
||||
|
||||
HorizontalAlignment = HAlignment.Left;
|
||||
VerticalExpand = true;
|
||||
|
||||
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},
|
||||
HorizontalAlignment = HAlignment.Left,
|
||||
VerticalAlignment = VAlignment.Top
|
||||
};
|
||||
AddChild(panelContainer);
|
||||
|
||||
var hotbarContainer = new VBoxContainer
|
||||
{
|
||||
SeparationOverride = 3,
|
||||
HorizontalAlignment = HAlignment.Left
|
||||
};
|
||||
panelContainer.AddChild(hotbarContainer);
|
||||
|
||||
var settingsContainer = new HBoxContainer
|
||||
{
|
||||
HorizontalExpand = true
|
||||
};
|
||||
hotbarContainer.AddChild(settingsContainer);
|
||||
|
||||
settingsContainer.AddChild(new Control { HorizontalExpand = true, SizeFlagsStretchRatio = 1 });
|
||||
_lockTexture = resourceCache.GetTexture("/Textures/Interface/Nano/lock.svg.192dpi.png");
|
||||
_unlockTexture = resourceCache.GetTexture("/Textures/Interface/Nano/lock_open.svg.192dpi.png");
|
||||
_lockButton = new TextureButton
|
||||
{
|
||||
TextureNormal = _unlockTexture,
|
||||
HorizontalAlignment = HAlignment.Center,
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
SizeFlagsStretchRatio = 1,
|
||||
Scale = (0.5f, 0.5f)
|
||||
};
|
||||
settingsContainer.AddChild(_lockButton);
|
||||
settingsContainer.AddChild(new Control { HorizontalExpand = true, SizeFlagsStretchRatio = 2 });
|
||||
_settingsButton = new TextureButton
|
||||
{
|
||||
TextureNormal = resourceCache.GetTexture("/Textures/Interface/Nano/gear.svg.192dpi.png"),
|
||||
HorizontalAlignment = HAlignment.Center,
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
SizeFlagsStretchRatio = 1,
|
||||
Scale = (0.5f, 0.5f)
|
||||
};
|
||||
settingsContainer.AddChild(_settingsButton);
|
||||
settingsContainer.AddChild(new Control { HorizontalExpand = true, SizeFlagsStretchRatio = 1 });
|
||||
|
||||
// this allows a 2 column layout if window gets too small
|
||||
_slotContainer = new GridContainer
|
||||
{
|
||||
MaxGridHeight = CalcMaxHeight()
|
||||
};
|
||||
hotbarContainer.AddChild(_slotContainer);
|
||||
|
||||
_loadoutContainer = new HBoxContainer
|
||||
{
|
||||
HorizontalExpand = true,
|
||||
MouseFilter = MouseFilterMode.Stop
|
||||
};
|
||||
hotbarContainer.AddChild(_loadoutContainer);
|
||||
|
||||
_loadoutContainer.AddChild(new Control { HorizontalExpand = true, SizeFlagsStretchRatio = 1 });
|
||||
var previousHotbarIcon = new TextureRect()
|
||||
{
|
||||
Texture = resourceCache.GetTexture("/Textures/Interface/Nano/left_arrow.svg.192dpi.png"),
|
||||
HorizontalAlignment = HAlignment.Center,
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
SizeFlagsStretchRatio = 1,
|
||||
TextureScale = (0.5f, 0.5f)
|
||||
};
|
||||
_loadoutContainer.AddChild(previousHotbarIcon);
|
||||
_loadoutContainer.AddChild(new Control { HorizontalExpand = true, SizeFlagsStretchRatio = 2 });
|
||||
_loadoutNumber = new Label
|
||||
{
|
||||
Text = "1",
|
||||
SizeFlagsStretchRatio = 1
|
||||
};
|
||||
_loadoutContainer.AddChild(_loadoutNumber);
|
||||
_loadoutContainer.AddChild(new Control { HorizontalExpand = true, SizeFlagsStretchRatio = 2 });
|
||||
var nextHotbarIcon = new TextureRect
|
||||
{
|
||||
Texture = resourceCache.GetTexture("/Textures/Interface/Nano/right_arrow.svg.192dpi.png"),
|
||||
HorizontalAlignment = HAlignment.Center,
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
SizeFlagsStretchRatio = 1,
|
||||
TextureScale = (0.5f, 0.5f)
|
||||
};
|
||||
_loadoutContainer.AddChild(nextHotbarIcon);
|
||||
_loadoutContainer.AddChild(new Control { HorizontalExpand = true, SizeFlagsStretchRatio = 1 });
|
||||
|
||||
_slots = new ActionSlot[ClientActionsComponent.Slots];
|
||||
|
||||
_dragShadow = new TextureRect
|
||||
{
|
||||
MinSize = (64, 64),
|
||||
Stretch = TextureRect.StretchMode.Scale,
|
||||
Visible = false,
|
||||
SetSize = (64, 64)
|
||||
};
|
||||
UserInterfaceManager.PopupRoot.AddChild(_dragShadow);
|
||||
|
||||
for (byte i = 0; i < ClientActionsComponent.Slots; i++)
|
||||
{
|
||||
var slot = new ActionSlot(this, _menu, actionsComponent, i);
|
||||
_slotContainer.AddChild(slot);
|
||||
_slots[i] = slot;
|
||||
}
|
||||
|
||||
DragDropHelper = new DragDropHelper<ActionSlot>(OnBeginActionDrag, OnContinueActionDrag, OnEndActionDrag);
|
||||
|
||||
MinSize = (10, 400);
|
||||
}
|
||||
|
||||
protected override void EnteredTree()
|
||||
{
|
||||
base.EnteredTree();
|
||||
_lockButton.OnPressed += OnLockPressed;
|
||||
_settingsButton.OnPressed += OnToggleActionsMenu;
|
||||
_loadoutContainer.OnKeyBindDown += OnHotbarPaginate;
|
||||
_gameHud.ActionsButtonToggled += OnToggleActionsMenuTopButton;
|
||||
_gameHud.ActionsButtonDown = false;
|
||||
_gameHud.ActionsButtonVisible = true;
|
||||
}
|
||||
|
||||
protected override void ExitedTree()
|
||||
{
|
||||
base.ExitedTree();
|
||||
StopTargeting();
|
||||
_menu.Close();
|
||||
_lockButton.OnPressed -= OnLockPressed;
|
||||
_settingsButton.OnPressed -= OnToggleActionsMenu;
|
||||
_loadoutContainer.OnKeyBindDown -= OnHotbarPaginate;
|
||||
_gameHud.ActionsButtonToggled -= OnToggleActionsMenuTopButton;
|
||||
_gameHud.ActionsButtonDown = false;
|
||||
_gameHud.ActionsButtonVisible = false;
|
||||
}
|
||||
|
||||
protected override void Resized()
|
||||
{
|
||||
base.Resized();
|
||||
_slotContainer.MaxGridHeight = 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.MaxGridHeight = 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 for this action and it's now on cooldown, stop targeting if we're supposed to
|
||||
if (SelectingTargetFor?.Action != null && SelectingTargetFor.Action == action &&
|
||||
actionState.IsOnCooldown(_gameTiming) && action.DeselectOnCooldown)
|
||||
{
|
||||
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 we should
|
||||
if (SelectingTargetFor?.Action != null && SelectingTargetFor.Action == action &&
|
||||
SelectingTargetFor.Item == itemEntity &&
|
||||
actionState.IsOnCooldown(_gameTiming) && action.DeselectOnCooldown)
|
||||
{
|
||||
StopTargeting();
|
||||
}
|
||||
}
|
||||
actionSlot.Cooldown = actionState.Cooldown;
|
||||
|
||||
// check if we need to toggle it
|
||||
if (action.BehaviorType == BehaviorType.Toggle)
|
||||
{
|
||||
actionSlot.ToggledOn = actionState.ToggledOn;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void OnHotbarPaginate(GUIBoundKeyEventArgs args)
|
||||
{
|
||||
// rather than clicking the arrows themselves, the user can click the hbox so it's more
|
||||
// "forgiving" for misclicks, and we simply check which side they are closer to
|
||||
if (args.Function != EngineKeyFunctions.UIClick) return;
|
||||
|
||||
var rightness = args.RelativePosition.X / _loadoutContainer.Width;
|
||||
if (rightness > 0.5)
|
||||
{
|
||||
ChangeHotbar((byte) ((SelectedHotbar + 1) % ClientActionsComponent.Hotbars));
|
||||
}
|
||||
else
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
private void OnToggleActionsMenuTopButton(bool open)
|
||||
{
|
||||
if (open == _menu.IsOpen) return;
|
||||
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.Position - (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.Position - (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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle hotbar change.
|
||||
/// </summary>
|
||||
/// <param name="hotbar">hotbar index to switch to</param>
|
||||
public void HandleChangeHotbarKeybind(byte hotbar, PointerInputCmdHandler.PointerInputCmdArgs args)
|
||||
{
|
||||
ChangeHotbar(hotbar);
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
DragDropHelper.Update(args.DeltaSeconds);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user