Files
tbd-station-14/Content.Client/Actions/UI/ActionsUI.cs
Leon Friedrich ff7d4ed9f6 Patched Actions Rework (#6899)
* Rejig Actions

* fix merge errors

* lambda-b-gon

* fix PAI, add innate actions

* Revert "fix PAI, add innate actions"

This reverts commit 4b501ac083e979e31ebd98d7b98077e0dbdd344b.

* Just fix by making nullable.

if only require: true actually did something somehow.

* Make AddActions() ensure an actions component

and misc comments

* misc cleanup

* Limit range even when not checking for obstructions

* remove old guardian code

* rename function and make EntityUid nullable

* fix magboot bug

* fix action search menu

* make targeting toggle all equivalent actions

* fix combat popups (enabling <-> disabling)

* fix networking

* Allow action locking

* prevent telepathy
2022-02-25 23:24:08 -06:00

501 lines
18 KiB
C#

using Content.Client.DragDrop;
using Content.Client.HUD;
using Content.Client.Resources;
using Content.Client.Stylesheets;
using Content.Shared.Actions;
using Content.Shared.Actions.ActionTypes;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.Utility;
using Robust.Shared.Input;
using Robust.Shared.Input.Binding;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using static Robust.Client.UserInterface.Controls.BoxContainer;
namespace Content.Client.Actions.UI
{
/// <summary>
/// The action hotbar on the left side of the screen.
/// </summary>
public sealed class ActionsUI : Container
{
private const float DragDeadZone = 10f;
private const float CustomTooltipDelay = 0.4f;
internal readonly ActionsSystem System;
private readonly IGameHud _gameHud;
/// <summary>
/// The action component of the currently attached entity.
/// </summary>
public readonly ActionsComponent Component;
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 BoxContainer _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(ActionsSystem system, ActionsComponent component)
{
SetValue(LayoutContainer.DebugProperty, true);
System = system;
Component = component;
_gameHud = IoCManager.Resolve<IGameHud>();
_menu = new ActionMenu(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 BoxContainer
{
Orientation = LayoutOrientation.Vertical,
SeparationOverride = 3,
HorizontalAlignment = HAlignment.Left
};
panelContainer.AddChild(hotbarContainer);
var settingsContainer = new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
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),
ToolTip = Loc.GetString("ui-actionsui-function-lock-action-slots"),
TooltipDelay = CustomTooltipDelay
};
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),
ToolTip = Loc.GetString("ui-actionsui-function-open-abilities-menu"),
TooltipDelay = CustomTooltipDelay
};
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 BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
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[ActionsSystem.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 < ActionsSystem.Slots; i++)
{
var slot = new ActionSlot(this, _menu, i);
_slotContainer.AddChild(slot);
_slots[i] = slot;
}
DragDropHelper = new DragDropHelper<ActionSlot>(OnBeginActionDrag, OnContinueActionDrag, OnEndActionDrag, DragDeadZone);
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 action = System.Assignments[SelectedHotbar, actionSlot.SlotIndex];
if (action == null)
{
if (SelectingTargetFor == actionSlot)
StopTargeting(true);
actionSlot.Clear();
continue;
}
if (Component.Actions.TryGetValue(action, out var actualAction))
{
UpdateActionSlot(actualAction, actionSlot);
continue;
}
// Action not in the actions component, but in the assignment list.
// This is either an action that doesn't auto-clear from the menu, or the action menu was locked.
// Show the old action, but make sure it is disabled;
action.Enabled = false;
action.Toggled = false;
// If we enable the item-sprite, and if the item-sprite has a visual toggle, then the player will be
// able to know whether the item is toggled, even if it is not in their LOS (but in PVS). And for things
// like PDA sprites, the player can even see whether the action's item is currently inside of their PVS.
// SO unless theres some way of "freezing" a sprite-view, we just have to disable it.
action.ItemIconStyle = ItemActionIconStyle.NoItem;
UpdateActionSlot(action, actionSlot);
}
}
private void UpdateActionSlot(ActionType action, ActionSlot actionSlot)
{
actionSlot.Assign(action);
if (!action.Enabled)
{
// just revoked an action we were trying to target with, stop targeting
if (SelectingTargetFor?.Action != null && SelectingTargetFor.Action == action)
{
StopTargeting();
}
actionSlot.Disable();
}
else
{
actionSlot.Enable();
}
actionSlot.UpdateIcons();
actionSlot.DrawModeChanged();
}
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) % ActionsSystem.Hotbars));
}
else
{
var newBar = SelectedHotbar == 0 ? ActionsSystem.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 (actionSlot.Action == null)
return;
// If we were targeting something else we should stop
StopTargeting();
SelectingTargetFor = actionSlot;
if (actionSlot.Action is TargetedAction targetAction)
System.StartTargeting(targetAction);
UpdateUI();
}
/// <summary>
/// Switch out of targeting mode if currently selecting target for an action
/// </summary>
public void StopTargeting(bool updating = false)
{
if (SelectingTargetFor == null)
return;
SelectingTargetFor = null;
System.StopTargeting();
// Sometimes targeting gets stopped mid-UI update.
// in that case, don't need to do a nested UI refresh.
if (!updating)
UpdateUI();
}
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);
actionSlot.DrawModeChanged();
}
/// <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);
}
}
}