Hud refactor (#7202)

Co-authored-by: DrSmugleaf <DrSmugleaf@users.noreply.github.com>
Co-authored-by: Jezithyr <jmaster9999@gmail.com>
Co-authored-by: Jezithyr <Jezithyr@gmail.com>
Co-authored-by: Visne <39844191+Visne@users.noreply.github.com>
Co-authored-by: wrexbe <wrexbe@protonmail.com>
Co-authored-by: wrexbe <81056464+wrexbe@users.noreply.github.com>
This commit is contained in:
Jezithyr
2022-10-12 01:16:23 -07:00
committed by GitHub
parent d09fbc1849
commit 571dd4e6d5
168 changed files with 6940 additions and 7817 deletions

View File

@@ -1,33 +1,17 @@
using Content.Client.Actions.Assignments;
using Content.Client.Actions.UI;
using Content.Client.Construction;
using Content.Client.DragDrop;
using Content.Client.Hands;
using Content.Client.Items.Managers;
using Content.Client.Outline;
using Content.Client.Popups;
using System.IO;
using Content.Shared.Actions;
using Content.Shared.Actions.ActionTypes;
using Content.Shared.Input;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Client.Utility;
using Robust.Shared.Audio;
using Robust.Shared.ContentPack;
using Robust.Shared.GameStates;
using Robust.Shared.Input;
using Robust.Shared.Input.Binding;
using Robust.Shared.Player;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Serialization.Markdown.Mapping;
using Robust.Shared.Serialization.Markdown.Sequence;
using Robust.Shared.Utility;
using System.IO;
using System.Linq;
using YamlDotNet.RepresentationModel;
namespace Content.Client.Actions
@@ -35,268 +19,84 @@ namespace Content.Client.Actions
[UsedImplicitly]
public sealed class ActionsSystem : SharedActionsSystem
{
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IUserInterfaceManager _uiManager = default!;
[Dependency] private readonly IItemSlotManager _itemSlotManager = default!;
[Dependency] private readonly ISerializationManager _serializationManager = default!;
[Dependency] private readonly IResourceManager _resourceManager = default!;
[Dependency] private readonly IOverlayManager _overlayMan = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly InteractionOutlineSystem _interactionOutline = default!;
[Dependency] private readonly TargetOutlineSystem _targetOutline = default!;
[Dependency] private readonly IResourceManager _resources = default!;
[Dependency] private readonly ISerializationManager _serialization = default!;
// TODO Redo assignments, including allowing permanent user configurable slot assignments.
/// <summary>
/// Current assignments for all hotbars / slots for this entity.
/// </summary>
public ActionAssignments Assignments = new(Hotbars, Slots);
public event Action<ActionType>? OnActionAdded;
public event Action<ActionType>? OnActionRemoved;
public event Action<ActionsComponent>? OnLinkActions;
public event Action? OnUnlinkActions;
public event Action? ClearAssignments;
public event Action<List<SlotAssignment>>? AssignSlot;
public const byte Hotbars = 9;
public const byte Slots = 10;
public bool UIDirty;
public ActionsUI? Ui;
private EntityUid? _highlightedEntity;
public ActionsComponent? PlayerActions { get; private set; }
public override void Initialize()
{
base.Initialize();
// set up hotkeys for hotbar
CommandBinds.Builder
.Bind(ContentKeyFunctions.OpenActionsMenu,
InputCmdHandler.FromDelegate(_ => ToggleActionsMenu()))
.Bind(ContentKeyFunctions.Hotbar1,
HandleHotbarKeybind(0))
.Bind(ContentKeyFunctions.Hotbar2,
HandleHotbarKeybind(1))
.Bind(ContentKeyFunctions.Hotbar3,
HandleHotbarKeybind(2))
.Bind(ContentKeyFunctions.Hotbar4,
HandleHotbarKeybind(3))
.Bind(ContentKeyFunctions.Hotbar5,
HandleHotbarKeybind(4))
.Bind(ContentKeyFunctions.Hotbar6,
HandleHotbarKeybind(5))
.Bind(ContentKeyFunctions.Hotbar7,
HandleHotbarKeybind(6))
.Bind(ContentKeyFunctions.Hotbar8,
HandleHotbarKeybind(7))
.Bind(ContentKeyFunctions.Hotbar9,
HandleHotbarKeybind(8))
.Bind(ContentKeyFunctions.Hotbar0,
HandleHotbarKeybind(9))
.Bind(ContentKeyFunctions.Loadout1,
HandleChangeHotbarKeybind(0))
.Bind(ContentKeyFunctions.Loadout2,
HandleChangeHotbarKeybind(1))
.Bind(ContentKeyFunctions.Loadout3,
HandleChangeHotbarKeybind(2))
.Bind(ContentKeyFunctions.Loadout4,
HandleChangeHotbarKeybind(3))
.Bind(ContentKeyFunctions.Loadout5,
HandleChangeHotbarKeybind(4))
.Bind(ContentKeyFunctions.Loadout6,
HandleChangeHotbarKeybind(5))
.Bind(ContentKeyFunctions.Loadout7,
HandleChangeHotbarKeybind(6))
.Bind(ContentKeyFunctions.Loadout8,
HandleChangeHotbarKeybind(7))
.Bind(ContentKeyFunctions.Loadout9,
HandleChangeHotbarKeybind(8))
// when selecting a target, we intercept clicks in the game world, treating them as our target selection. We want to
// take priority before any other systems handle the click.
.BindBefore(EngineKeyFunctions.Use, new PointerInputCmdHandler(TargetingOnUse, outsidePrediction: true),
typeof(ConstructionSystem), typeof(DragDropSystem))
.BindBefore(EngineKeyFunctions.UIRightClick, new PointerInputCmdHandler(TargetingCancel, outsidePrediction: true))
.Register<ActionsSystem>();
SubscribeLocalEvent<ActionsComponent, PlayerAttachedEvent>(OnPlayerAttached);
SubscribeLocalEvent<ActionsComponent, PlayerDetachedEvent>(OnPlayerDetached);
SubscribeLocalEvent<ActionsComponent, ComponentHandleState>(HandleState);
SubscribeLocalEvent<ActionsComponent, ComponentHandleState>(HandleComponentState);
}
public override void Dirty(ActionType action)
private void HandleComponentState(EntityUid uid, ActionsComponent component, ref ComponentHandleState args)
{
// Should only ever receive component states for attached player's component.
// --> lets not bother unnecessarily dirtying and prediction-resetting actions for other players.
if (action.AttachedEntity != _playerManager.LocalPlayer?.ControlledEntity)
if (args.Current is not ActionsComponentState currentState)
return;
base.Dirty(action);
UIDirty = true;
}
List<ActionType> added = new();
List<ActionType> removed = new();
private void HandleState(EntityUid uid, ActionsComponent component, ref ComponentHandleState args)
{
if (args.Current is not ActionsComponentState state)
return;
var serverActions = new SortedSet<ActionType>(state.Actions);
foreach (var act in component.Actions.ToList())
foreach (var actionType in component.Actions)
{
if (act.ClientExclusive)
continue;
if (!serverActions.TryGetValue(act, out var serverAct))
if (!currentState.Actions.Contains(actionType))
{
component.Actions.Remove(act);
if (act.AutoRemove && !(Ui?.Locked ?? false))
Assignments.Remove(act);
continue;
removed.Add(actionType);
}
act.CopyFrom(serverAct);
serverActions.Remove(serverAct);
}
// Anything that remains is a new action
foreach (var newAct in serverActions)
foreach (var serverAction in currentState.Actions)
{
// We create a new action, not just sorting a reference to the state's action.
component.Actions.Add((ActionType) newAct.Clone());
}
UIDirty = true;
}
/// <summary>
/// Highlights the item slot (inventory or hand) that contains this item
/// </summary>
/// <param name="item"></param>
public void HighlightItemSlot(EntityUid item)
{
StopHighlightingItemSlot();
_highlightedEntity = item;
_itemSlotManager.HighlightEntity(item);
}
/// <summary>
/// Stops highlighting any item slots we are currently highlighting.
/// </summary>H
public void StopHighlightingItemSlot()
{
if (_highlightedEntity == null)
return;
_itemSlotManager.UnHighlightEntity(_highlightedEntity.Value);
_highlightedEntity = null;
}
protected override void AddActionInternal(ActionsComponent comp, ActionType action)
{
// Sometimes the client receives actions from the server, before predicting that newly added components will add
// their own shared actions. Just in case those systems ever decided to directly access action properties (e.g.,
// action.Toggled), we will remove duplicates:
if (comp.Actions.TryGetValue(action, out var existing))
{
comp.Actions.Remove(existing);
Assignments.Replace(existing, action);
}
comp.Actions.Add(action);
}
public override void AddAction(EntityUid uid, ActionType action, EntityUid? provider, ActionsComponent? comp = null, bool dirty = true)
{
if (!Resolve(uid, ref comp, false))
return;
base.AddAction(uid, action, provider, comp, dirty);
if (uid == _playerManager.LocalPlayer?.ControlledEntity)
UIDirty = true;
}
public override void RemoveActions(EntityUid uid, IEnumerable<ActionType> actions, ActionsComponent? comp = null, bool dirty = true)
{
if (uid != _playerManager.LocalPlayer?.ControlledEntity)
return;
if (!Resolve(uid, ref comp, false))
return;
base.RemoveActions(uid, actions, comp, dirty);
foreach (var act in actions)
{
if (act.AutoRemove && !(Ui?.Locked ?? false))
Assignments.Remove(act);
}
UIDirty = true;
}
public override void FrameUpdate(float frameTime)
{
// avoid updating GUI when doing predictions & resetting state.
if (UIDirty)
{
UIDirty = false;
UpdateUI();
}
}
/// <summary>
/// Updates the displayed hotbar (and menu) based on current state of actions.
/// </summary>
public void UpdateUI()
{
if (Ui == null)
return;
foreach (var action in Ui.Component.Actions)
{
if (action.AutoPopulate && !Assignments.Assignments.ContainsKey(action))
Assignments.AutoPopulate(action, Ui.SelectedHotbar, false);
}
// get rid of actions that are no longer available to the user
foreach (var (action, index) in Assignments.Assignments.ToList())
{
if (index.Count == 0)
if (!component.Actions.TryGetValue(serverAction, out var clientAction))
{
Assignments.Assignments.Remove(action);
continue;
added.Add((ActionType) serverAction.Clone());
}
else
{
clientAction.CopyFrom(serverAction);
}
if (action.AutoRemove && !Ui.Locked && !Ui.Component.Actions.Contains(action))
Assignments.ClearSlot(index[0].Hotbar, index[0].Slot, false);
}
Assignments.PreventAutoPopulate.RemoveWhere(action => !Ui.Component.Actions.Contains(action));
foreach (var actionType in added)
{
component.Actions.Add(actionType);
OnActionAdded?.Invoke(actionType);
}
Ui.UpdateUI();
}
public void HandleHotbarKeybind(byte slot, in PointerInputCmdHandler.PointerInputCmdArgs args)
{
Ui?.HandleHotbarKeybind(slot, args);
}
public void HandleChangeHotbarKeybind(byte hotbar, in PointerInputCmdHandler.PointerInputCmdArgs args)
{
Ui?.HandleChangeHotbarKeybind(hotbar, args);
}
private void OnPlayerDetached(EntityUid uid, ActionsComponent component, PlayerDetachedEvent args)
{
if (Ui == null) return;
_uiManager.StateRoot.RemoveChild(Ui);
Ui = null;
foreach (var actionType in removed)
{
component.Actions.Remove(actionType);
OnActionRemoved?.Invoke(actionType);
}
}
private void OnPlayerAttached(EntityUid uid, ActionsComponent component, PlayerAttachedEvent args)
{
Assignments = new(Hotbars, Slots);
Ui = new ActionsUI(this, component);
_uiManager.StateRoot.AddChild(Ui);
UIDirty = true;
if (uid != _playerManager.LocalPlayer?.ControlledEntity)
return;
OnLinkActions?.Invoke(component);
PlayerActions = component;
}
private void OnPlayerDetached(EntityUid uid, ActionsComponent component, PlayerDetachedEvent? args = null)
{
if (uid != _playerManager.LocalPlayer?.ControlledEntity)
return;
OnUnlinkActions?.Invoke();
PlayerActions = null;
}
public override void Shutdown()
@@ -305,70 +105,25 @@ namespace Content.Client.Actions
CommandBinds.Unregister<ActionsSystem>();
}
private PointerInputCmdHandler HandleHotbarKeybind(byte slot)
public void TriggerAction(ActionType? action)
{
// delegate to the ActionsUI, simulating a click on it
return new((in PointerInputCmdHandler.PointerInputCmdArgs args) =>
{
var playerEntity = _playerManager.LocalPlayer?.ControlledEntity;
if (playerEntity == null ||
!EntityManager.TryGetComponent<ActionsComponent?>(playerEntity.Value, out var actionsComponent)) return false;
HandleHotbarKeybind(slot, args);
return true;
}, false);
}
private PointerInputCmdHandler HandleChangeHotbarKeybind(byte hotbar)
{
// delegate to the ActionsUI, simulating a click on it
return new((in PointerInputCmdHandler.PointerInputCmdArgs args) =>
{
var playerEntity = _playerManager.LocalPlayer?.ControlledEntity;
if (!EntityManager.TryGetComponent<ActionsComponent?>(playerEntity, out var actionsComponent)) return false;
HandleChangeHotbarKeybind(hotbar, args);
return true;
},
false);
}
private void ToggleActionsMenu()
{
Ui?.ToggleActionsMenu();
}
/// <summary>
/// A action slot was pressed. This either performs the action or toggles the targeting mode.
/// </summary>
internal void OnSlotPressed(ActionSlot slot)
{
if (Ui == null)
if (PlayerActions == null || action == null || _playerManager.LocalPlayer?.ControlledEntity is not { Valid: true } user)
return;
if (slot.Action == null || _playerManager.LocalPlayer?.ControlledEntity is not EntityUid user)
if (action.Provider != null && Deleted(action.Provider))
return;
if (slot.Action.Provider != null && Deleted(slot.Action.Provider))
return;
if (slot.Action is not InstantAction instantAction)
if (action is not InstantAction instantAction)
{
// for target actions, we go into "select target" mode, we don't
// message the server until we actually pick our target.
// if we're clicking the same thing we're already targeting for, then we simply cancel
// targeting
Ui.ToggleTargeting(slot);
return;
}
if (slot.Action.ClientExclusive)
if (action.ClientExclusive)
{
if (instantAction.Event != null)
instantAction.Event.Performer = user;
PerformAction(Ui.Component, instantAction, instantAction.Event, GameTiming.CurTime);
PerformAction(PlayerActions, instantAction, instantAction.Event, GameTiming.CurTime);
}
else
{
@@ -377,226 +132,6 @@ namespace Content.Client.Actions
}
}
private bool TargetingCancel(in PointerInputCmdHandler.PointerInputCmdArgs args)
{
if (!GameTiming.IsFirstTimePredicted)
return false;
// only do something for actual target-based actions
if (Ui?.SelectingTargetFor?.Action == null)
return false;
Ui.StopTargeting();
return true;
}
/// <summary>
/// If the user clicked somewhere, and they are currently targeting an action, try and perform it.
/// </summary>
private bool TargetingOnUse(in PointerInputCmdHandler.PointerInputCmdArgs args)
{
if (!GameTiming.IsFirstTimePredicted)
return false;
// only do something for actual target-based actions
if (Ui?.SelectingTargetFor?.Action is not TargetedAction action)
return false;
if (_playerManager.LocalPlayer?.ControlledEntity is not EntityUid user)
return false;
if (!TryComp(user, out ActionsComponent? comp))
return false;
// Is the action currently valid?
if (!action.Enabled
|| action.Charges != null && action.Charges == 0
|| action.Cooldown.HasValue && action.Cooldown.Value.End > GameTiming.CurTime)
{
// The user is targeting with this action, but it is not valid. Maybe mark this click as
// handled and prevent further interactions.
return !action.InteractOnMiss;
}
switch (action)
{
case WorldTargetAction mapTarget:
return TryTargetWorld(args, mapTarget, user, comp) || !action.InteractOnMiss;
case EntityTargetAction entTarget:
return TargetEntity(args, entTarget, user, comp) || !action.InteractOnMiss;
default:
Logger.Error($"Unknown targeting action: {action.GetType()}");
return false;
}
}
private bool TryTargetWorld(in PointerInputCmdHandler.PointerInputCmdArgs args, WorldTargetAction action, EntityUid user, ActionsComponent actionComp)
{
var coords = args.Coordinates.ToMap(EntityManager);
if (!ValidateWorldTarget(user, coords, action))
{
// Invalid target.
if (action.DeselectOnMiss)
Ui?.StopTargeting();
return false;
}
if (action.ClientExclusive)
{
if (action.Event != null)
{
action.Event.Target = coords;
action.Event.Performer = user;
}
PerformAction(actionComp, action, action.Event, GameTiming.CurTime);
}
else
EntityManager.RaisePredictiveEvent(new RequestPerformActionEvent(action, coords));
if (!action.Repeat)
Ui?.StopTargeting();
return true;
}
private bool TargetEntity(in PointerInputCmdHandler.PointerInputCmdArgs args, EntityTargetAction action, EntityUid user, ActionsComponent actionComp)
{
if (!ValidateEntityTarget(user, args.EntityUid, action))
{
if (action.DeselectOnMiss)
Ui?.StopTargeting();
return false;
}
if (action.ClientExclusive)
{
if (action.Event != null)
{
action.Event.Target = args.EntityUid;
action.Event.Performer = user;
}
PerformAction(actionComp, action, action.Event, GameTiming.CurTime);
}
else
EntityManager.RaisePredictiveEvent(new RequestPerformActionEvent(action, args.EntityUid));
if (!action.Repeat)
Ui?.StopTargeting();
return true;
}
/// <summary>
/// Execute convenience functionality for actions (pop-ups, sound, speech)
/// </summary>
protected override bool PerformBasicActions(EntityUid user, ActionType action)
{
var performedAction = action.Sound != null
|| !string.IsNullOrWhiteSpace(action.UserPopup)
|| !string.IsNullOrWhiteSpace(action.Popup);
if (!GameTiming.IsFirstTimePredicted)
return performedAction;
if (!string.IsNullOrWhiteSpace(action.UserPopup))
{
var msg = (!action.Toggled || string.IsNullOrWhiteSpace(action.PopupToggleSuffix))
? Loc.GetString(action.UserPopup)
: Loc.GetString(action.UserPopup + action.PopupToggleSuffix);
_popupSystem.PopupEntity(msg, user);
}
else if (!string.IsNullOrWhiteSpace(action.Popup))
{
var msg = (!action.Toggled || string.IsNullOrWhiteSpace(action.PopupToggleSuffix))
? Loc.GetString(action.Popup)
: Loc.GetString(action.Popup + action.PopupToggleSuffix);
_popupSystem.PopupEntity(msg, user);
}
if (action.Sound != null)
SoundSystem.Play(action.Sound.GetSound(), Filter.Local(), user, action.AudioParams);
return performedAction;
}
internal void StopTargeting()
{
_targetOutline.Disable();
_interactionOutline.SetEnabled(true);
if (!_overlayMan.TryGetOverlay<ShowHandItemOverlay>(out var handOverlay) || handOverlay == null)
return;
handOverlay.IconOverride = null;
handOverlay.EntityOverride = null;
}
internal void StartTargeting(TargetedAction action)
{
// override "held-item" overlay
if (action.TargetingIndicator
&& _overlayMan.TryGetOverlay<ShowHandItemOverlay>(out var handOverlay)
&& handOverlay != null)
{
if (action.ItemIconStyle == ItemActionIconStyle.BigItem && action.Provider != null)
{
handOverlay.EntityOverride = action.Provider;
}
else if (action.Toggled && action.IconOn != null)
handOverlay.IconOverride = action.IconOn.Frame0();
else if (action.Icon != null)
handOverlay.IconOverride = action.Icon.Frame0();
}
// TODO: allow world-targets to check valid positions. E.g., maybe:
// - Draw a red/green ghost entity
// - Add a yes/no checkmark where the HandItemOverlay usually is
// Highlight valid entity targets
if (action is not EntityTargetAction entityAction)
return;
Func<EntityUid, bool>? predicate = null;
if (!entityAction.CanTargetSelf)
predicate = e => e != entityAction.AttachedEntity;
var range = entityAction.CheckCanAccess ? action.Range : -1;
_interactionOutline.SetEnabled(false);
_targetOutline.Enable(range, entityAction.CheckCanAccess, predicate, entityAction.Whitelist, null);
}
internal void TryFillSlot(byte hotbar, byte index)
{
if (Ui == null)
return;
var fillEvent = new FillActionSlotEvent();
RaiseLocalEvent(Ui.Component.Owner, fillEvent, broadcast: true);
if (fillEvent.Action == null)
return;
fillEvent.Action.ClientExclusive = true;
fillEvent.Action.Temporary = true;
fillEvent.Action.AutoPopulate = false;
Ui.Component.Actions.Add(fillEvent.Action);
Assignments.AssignSlot(hotbar, index, fillEvent.Action);
Ui.UpdateUI();
}
/*public void SaveActionAssignments(string path)
{
@@ -624,13 +159,13 @@ namespace Content.Client.Actions
/// </summary>
public void LoadActionAssignments(string path, bool userData)
{
if (Ui == null)
if (PlayerActions == null)
return;
var file = new ResourcePath(path).ToRootedPath();
TextReader reader = userData
? _resourceManager.UserData.OpenText(file)
: _resourceManager.ContentFileReadText(file);
? _resources.UserData.OpenText(file)
: _resources.ContentFileReadText(file);
var yamlStream = new YamlStream();
yamlStream.Load(reader);
@@ -638,13 +173,9 @@ namespace Content.Client.Actions
if (yamlStream.Documents[0].RootNode.ToDataNode() is not SequenceDataNode sequence)
return;
foreach (var (action, assigns) in Assignments.Assignments)
{
foreach (var index in assigns)
{
Assignments.ClearSlot(index.Hotbar, index.Slot, true);
}
}
ClearAssignments?.Invoke();
var assignments = new List<SlotAssignment>();
foreach (var entry in sequence.Sequence)
{
@@ -654,28 +185,33 @@ namespace Content.Client.Actions
if (!map.TryGet("action", out var actionNode))
continue;
var action = _serializationManager.Read<ActionType>(actionNode);
var action = _serialization.Read<ActionType>(actionNode);
if (Ui.Component.Actions.TryGetValue(action, out var existingAction))
if (PlayerActions.Actions.TryGetValue(action, out var existingAction))
{
existingAction.CopyFrom(action);
action = existingAction;
}
else
Ui.Component.Actions.Add(action);
{
PlayerActions.Actions.Add(action);
}
if (!map.TryGet("assignments", out var assignmentNode))
continue;
var assignments = _serializationManager.Read<List<(byte Hotbar, byte Slot)>>(assignmentNode);
var nodeAssignments = _serialization.Read<List<(byte Hotbar, byte Slot)>>(assignmentNode);
foreach (var index in assignments)
foreach (var index in nodeAssignments)
{
Assignments.AssignSlot(index.Hotbar, index.Slot, action);
var assignment = new SlotAssignment(index.Hotbar, index.Slot, action);
assignments.Add(assignment);
}
}
UIDirty = true;
AssignSlot?.Invoke(assignments);
}
public record struct SlotAssignment(byte Hotbar, byte Slot, ActionType Action);
}
}

View File

@@ -1,167 +0,0 @@
using Content.Shared.Actions;
using Content.Shared.Actions.ActionTypes;
using System.Linq;
namespace Content.Client.Actions.Assignments
{
/// <summary>
/// Tracks and manages the hotbar assignments for actions.
/// </summary>
[DataDefinition]
public sealed class ActionAssignments
{
// the slots and assignments fields hold client's assignments (what action goes in what slot),
// which are completely client side and independent of what actions they've actually been granted and
// what item the action is actually for.
/// <summary>
/// x = hotbar number, y = slot of that hotbar (index 0 corresponds to the one labeled "1",
/// index 9 corresponds to the one labeled "0"). Essentially the inverse of _assignments.
/// </summary>
private readonly ActionType?[,] _slots;
/// <summary>
/// Hotbar and slot assignment for each action type (slot index 0 corresponds to the one labeled "1",
/// slot index 9 corresponds to the one labeled "0"). The key corresponds to an index in the _slots array.
/// The value is a list because actions can be assigned to multiple slots. Even if an action type has not been granted,
/// it can still be assigned to a slot. Essentially the inverse of _slots.
/// There will be no entry if there is no assignment (no empty lists in this dict)
/// </summary>
[DataField("assignments")]
public readonly Dictionary<ActionType, List<(byte Hotbar, byte Slot)>> Assignments = new();
/// <summary>
/// Actions which have been manually cleared by the user, thus should not
/// auto-populate.
/// </summary>
public readonly SortedSet<ActionType> PreventAutoPopulate = new();
private readonly byte _numHotbars;
private readonly byte _numSlots;
public ActionAssignments(byte numHotbars, byte numSlots)
{
_numHotbars = numHotbars;
_numSlots = numSlots;
_slots = new ActionType?[numHotbars, numSlots];
}
public bool Remove(ActionType action) => Replace(action, null);
internal bool Replace(ActionType action, ActionType? newAction)
{
if (!Assignments.Remove(action, out var assigns))
return false;
if (newAction != null)
Assignments[newAction] = assigns;
foreach (var (bar, slot) in assigns)
{
_slots[bar, slot] = newAction;
}
return true;
}
/// <summary>
/// Assigns the indicated hotbar slot to the specified action type.
/// </summary>
/// <param name="hotbar">hotbar whose slot is being assigned</param>
/// <param name="slot">slot of the hotbar to assign to (0 = the slot labeled 1, 9 = the slot labeled 0)</param>
/// <param name="actionType">action to assign to the slot</param>
public void AssignSlot(byte hotbar, byte slot, ActionType actionType)
{
ClearSlot(hotbar, slot, false);
_slots[hotbar, slot] = actionType;
if (Assignments.TryGetValue(actionType, out var slotList))
{
slotList.Add((hotbar, slot));
}
else
{
var newList = new List<(byte Hotbar, byte Slot)> { (hotbar, slot) };
Assignments[actionType] = newList;
}
}
/// <summary>
/// Clear the assignment from the indicated slot.
/// </summary>
/// <param name="hotbar">hotbar whose slot is being cleared</param>
/// <param name="slot">slot of the hotbar to clear (0 = the slot labeled 1, 9 = the slot labeled 0)</param>
/// <param name="preventAutoPopulate">if true, the action assigned to this slot
/// will be prevented from being auto-populated in the future when it is newly granted.
/// Item actions will automatically be allowed to auto populate again
/// when their associated item are unequipped. This ensures that items that are newly
/// picked up will always present their actions to the user even if they had earlier been cleared.
/// </param>
public void ClearSlot(byte hotbar, byte slot, bool preventAutoPopulate)
{
// remove this particular assignment from our data structures
// (keeping in mind something can be assigned multiple slots)
var currentAction = _slots[hotbar, slot];
if (currentAction == null)
return;
if (preventAutoPopulate)
PreventAutoPopulate.Add(currentAction);
var assignmentList = Assignments[currentAction];
assignmentList = assignmentList.Where(a => a.Hotbar != hotbar || a.Slot != slot).ToList();
if (!assignmentList.Any())
{
Assignments.Remove(currentAction);
}
else
{
Assignments[currentAction] = assignmentList;
}
_slots[hotbar, slot] = null;
}
/// <summary>
/// Finds the next open slot the action can go in and assigns it there,
/// starting from the currently selected hotbar.
/// Does not update any UI elements, only updates the assignment data structures.
/// </summary>
/// <param name="force">if true, will force the assignment to occur
/// regardless of whether this assignment has been prevented from auto population
/// via ClearSlot's preventAutoPopulate parameter. If false, will have no effect
/// if this assignment has been prevented from auto population.</param>
public void AutoPopulate(ActionType toAssign, byte currentHotbar, bool force = true)
{
if (!force && PreventAutoPopulate.Contains(toAssign))
return;
for (byte hotbarOffset = 0; hotbarOffset < _numHotbars; hotbarOffset++)
{
for (byte slot = 0; slot < _numSlots; slot++)
{
var hotbar = (byte) ((currentHotbar + hotbarOffset) % _numHotbars);
var slotAssignment = _slots[hotbar, slot];
if (slotAssignment != null)
continue;
AssignSlot(hotbar, slot, toAssign);
return;
}
}
// there was no empty slot
}
/// <summary>
/// Gets the assignment to the indicated slot if there is one.
/// </summary>
public ActionType? this[in byte hotbar, in byte slot] => _slots[hotbar, slot];
/// <returns>true if we have the assignment assigned to some slot</returns>
public bool HasAssignment(ActionType assignment)
{
return Assignments.ContainsKey(assignment);
}
}
}

View File

@@ -1,426 +0,0 @@
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Content.Client.DragDrop;
using Content.Client.HUD;
using Content.Client.Stylesheets;
using Content.Shared.Actions;
using Content.Shared.Actions.ActionTypes;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.Utility;
using Robust.Shared.Input;
using Robust.Shared.Timing;
using static Robust.Client.UserInterface.Controls.BaseButton;
using static Robust.Client.UserInterface.Controls.BoxContainer;
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 sealed class ActionMenu : DefaultWindow
{
// Pre-defined global filters that can be used to select actions based on their properties (as opposed to their
// own yaml-defined filters).
// TODO LOC STRINGs
private const string AllFilter = "all";
private const string ItemFilter = "item";
private const string InnateFilter = "innate";
private const string EnabledFilter = "enabled";
private const string InstantFilter = "instant";
private const string TargetedFilter = "targeted";
private readonly string[] _filters =
{
AllFilter,
ItemFilter,
InnateFilter,
EnabledFilter,
InstantFilter,
TargetedFilter
};
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);
/// <summary>
/// Is an action currently being dragged from this window?
/// </summary>
public bool IsDragging => _dragDropHelper.IsDragging;
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;
private readonly IEntityManager _entMan;
public ActionMenu(ActionsUI actionsUI)
{
_actionsUI = actionsUI;
_gameHud = IoCManager.Resolve<IGameHud>();
_entMan = IoCManager.Resolve<IEntityManager>();
Title = Loc.GetString("ui-actionmenu-title");
MinSize = (320, 300);
Contents.AddChild(new BoxContainer
{
Orientation = LayoutOrientation.Vertical,
Children =
{
new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
Children =
{
(_searchBar = new LineEdit
{
StyleClasses = { StyleNano.StyleClassActionSearchBox },
HorizontalExpand = true,
PlaceHolder = Loc.GetString("ui-actionmenu-search-bar-placeholder-text")
}),
(_filterButton = new MultiselectOptionButton<string>()
{
Label = Loc.GetString("ui-actionmenu-filter-button")
})
}
},
(_clearButton = new Button
{
Text = Loc.GetString("ui-actionmenu-clear-button"),
}),
(_filterLabel = new Label()),
new ScrollContainer
{
//TODO: needed? MinSize = new Vector2(200.0f, 0.0f),
VerticalExpand = true,
HorizontalExpand = true,
Children =
{
(_resultsGrid = new GridContainer
{
MaxGridWidth = 300
})
}
}
}
});
foreach (var tag in _filters)
{
_filterButton.AddItem( CultureInfo.CurrentCulture.TextInfo.ToTitleCase(tag), tag);
}
// default to showing all actions.
_filterButton.SelectKey(AllFilter);
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;
}
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;
}
_actionsUI.System.Assignments.AssignSlot(_actionsUI.SelectedHotbar, targetSlot.SlotIndex, _dragDropHelper.Dragged.Action);
_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;
_actionsUI.System.Assignments.AutoPopulate(actionMenuItem.Action, _actionsUI.SelectedHotbar);
_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 = _actionsUI.Component.Actions
.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("ui-actionmenu-filter-label",
("selectedLabels", string.Join(", ", _filterButton.SelectedLabels)));
}
}
private bool MatchesSearchCriteria(ActionType 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(action.DisplayName.ToString()).Contains(standardizedSearch))
{
return true;
}
// search by provider name
if (action.Provider == null || action.Provider == _actionsUI.Component.Owner)
return false;
var name = _entMan.GetComponent<MetaDataComponent>(action.Provider.Value).EntityName;
return Standardize(name).Contains(standardizedSearch);
}
private bool ActionMatchesFilterTag(ActionType action, string tag)
{
return tag switch
{
EnabledFilter => action.Enabled,
ItemFilter => action.Provider != null && action.Provider != _actionsUI.Component.Owner,
InnateFilter => action.Provider == null || action.Provider == _actionsUI.Component.Owner,
InstantFilter => action is InstantAction,
TargetedFilter => action is TargetedAction,
_ => true
};
}
/// <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 ??= string.Empty;
// 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<ActionType> actions)
{
ClearList();
foreach (var action in actions)
{
var actionItem = new ActionMenuItem(_actionsUI, action, OnItemFocusExited);
_resultsGrid.Children.Add(actionItem);
actionItem.SetActionState(action.Enabled);
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();
}
/// <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(actionMenuItem.Action.Enabled);
}
SearchAndDisplay();
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
_dragDropHelper.Update(args.DeltaSeconds);
}
}
}

View File

@@ -1,228 +0,0 @@
using System;
using Content.Client.Stylesheets;
using Content.Shared.Actions;
using Content.Shared.Actions.ActionTypes;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.Utility;
using Robust.Shared.Utility;
using static Robust.Client.UserInterface.Controls.BoxContainer;
namespace Content.Client.Actions.UI
{
// TODO merge this with action-slot when it gets XAMLd
// this has way too much overlap, especially now that they both have the item-sprite icons.
/// <summary>
/// An individual action visible in the action menu.
/// </summary>
public sealed class ActionMenuItem : ContainerButton
{
// shorter than default tooltip delay so user can
// quickly explore what each action is
private const float CustomTooltipDelay = 0.2f;
private readonly TextureRect _bigActionIcon;
private readonly TextureRect _smallActionIcon;
private readonly SpriteView _smallItemSpriteView;
private readonly SpriteView _bigItemSpriteView;
public ActionType Action;
private Action<ActionMenuItem> _onControlFocusExited;
private readonly ActionsUI _actionsUI;
public ActionMenuItem(ActionsUI actionsUI, ActionType action, Action<ActionMenuItem> onControlFocusExited)
{
_actionsUI = actionsUI;
Action = action;
_onControlFocusExited = onControlFocusExited;
SetSize = (64, 64);
VerticalAlignment = VAlignment.Top;
_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,
OverrideDirection = Direction.South,
};
// padding to the left of the small icon
var paddingBoxItemIcon = new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
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(paddingBoxItemIcon);
TooltipDelay = CustomTooltipDelay;
TooltipSupplier = SupplyTooltip;
UpdateIcons();
}
public void UpdateIcons()
{
UpdateItemIcon();
if (Action == null)
{
SetActionIcon(null);
return;
}
if ((_actionsUI.SelectingTargetFor?.Action == Action || Action.Toggled) && Action.IconOn != null)
SetActionIcon(Action.IconOn.Frame0());
else
SetActionIcon(Action.Icon?.Frame0());
}
private void SetActionIcon(Texture? texture)
{
if (texture == null || Action == null)
{
_bigActionIcon.Texture = null;
_bigActionIcon.Visible = false;
_smallActionIcon.Texture = null;
_smallActionIcon.Visible = false;
}
else if (Action.EntityIcon != null && Action.ItemIconStyle == ItemActionIconStyle.BigItem)
{
_smallActionIcon.Texture = texture;
_smallActionIcon.Modulate = Action.IconColor;
_smallActionIcon.Visible = true;
_bigActionIcon.Texture = null;
_bigActionIcon.Visible = false;
}
else
{
_bigActionIcon.Texture = texture;
_bigActionIcon.Modulate = Action.IconColor;
_bigActionIcon.Visible = true;
_smallActionIcon.Texture = null;
_smallActionIcon.Visible = false;
}
}
private void UpdateItemIcon()
{
if (Action?.EntityIcon == null || !IoCManager.Resolve<IEntityManager>().TryGetComponent(Action.EntityIcon.Value, out SpriteComponent? sprite))
{
_bigItemSpriteView.Visible = false;
_bigItemSpriteView.Sprite = null;
_smallItemSpriteView.Visible = false;
_smallItemSpriteView.Sprite = null;
}
else
{
switch (Action.ItemIconStyle)
{
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;
}
}
}
protected override void ControlFocusExited()
{
base.ControlFocusExited();
_onControlFocusExited.Invoke(this);
}
private Control SupplyTooltip(Control? sender)
{
var name = FormattedMessage.FromMarkupPermissive(Loc.GetString(Action.DisplayName));
var decr = FormattedMessage.FromMarkupPermissive(Loc.GetString(Action.Description));
var tooltip = new ActionAlertTooltip(name, decr);
if (Action.Enabled && (Action.Charges == null || Action.Charges != 0))
tooltip.Cooldown = Action.Cooldown;
return tooltip;
}
/// <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

@@ -1,551 +0,0 @@
using System;
using Content.Client.Cooldown;
using Content.Client.Stylesheets;
using Content.Shared.Actions;
using Content.Shared.Actions.ActionTypes;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.Utility;
using Robust.Shared.Input;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using static Robust.Client.UserInterface.Controls.BoxContainer;
namespace Content.Client.Actions.UI
{
/// <summary>
/// A slot in the action hotbar. Not extending BaseButton because
/// its needs diverged too much.
/// </summary>
public sealed 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";
private bool _spriteViewDirty = false;
/// <summary>
/// Current action in this slot.
/// </summary>
public ActionType? Action { get; private set; }
/// <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; }
private readonly IGameTiming _gameTiming;
private readonly IEntityManager _entMan;
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;
// 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, byte slotIndex, IGameTiming timing, IEntityManager entMan)
{
_actionsUI = actionsUI;
_actionMenu = actionMenu;
_gameTiming = timing;
_entMan = entMan;
SlotIndex = slotIndex;
MouseFilter = MouseFilterMode.Stop;
SetSize = (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,
OverrideDirection = Direction.South,
};
_cooldownGraphic = new CooldownGraphic {Progress = 0, Visible = false};
// padding to the left of the number to shift it right
var paddingBox = new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
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 BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
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)
{
if (Action == null)
return null;
string? extra = null;
if (Action.Charges != null)
{
extra = Loc.GetString("ui-actionslot-charges", ("charges", Action.Charges));
}
var name = FormattedMessage.FromMarkupPermissive(Loc.GetString(Action.DisplayName));
var decr = FormattedMessage.FromMarkupPermissive(Loc.GetString(Action.Description));
var tooltip = new ActionAlertTooltip(name, decr, extra);
if (Action.Enabled && (Action.Charges == null || Action.Charges != 0))
tooltip.Cooldown = Action.Cooldown;
return tooltip;
}
protected override void MouseEntered()
{
base.MouseEntered();
_beingHovered = true;
DrawModeChanged();
if (Action?.Provider != null)
_actionsUI.System.HighlightItemSlot(Action.Provider.Value);
}
protected override void MouseExited()
{
base.MouseExited();
_beingHovered = false;
CancelPress();
DrawModeChanged();
_actionsUI.System.StopHighlightingItemSlot();
}
protected override void KeyBindDown(GUIBoundKeyEventArgs args)
{
base.KeyBindDown(args);
if (Action == null)
{
// No action for this slot. Maybe the user is trying to add a mapping action?
_actionsUI.System.TryFillSlot(_actionsUI.SelectedHotbar, SlotIndex);
return;
}
// only handle clicks, and can't do anything to this if no assignment
if (args.Function == EngineKeyFunctions.UIClick)
{
// might turn into a drag or a full press if released
Depress(true);
_actionsUI.DragDropHelper.MouseDown(this);
DrawModeChanged();
return;
}
if (args.Function != EngineKeyFunctions.UIRightClick || _actionsUI.Locked)
return;
if (_actionsUI.DragDropHelper.IsDragging || _actionMenu.IsDragging)
return;
// user right clicked on an action slot, so we clear it.
_actionsUI.System.Assignments.ClearSlot(_actionsUI.SelectedHotbar, SlotIndex, true);
// If this was a temporary action, and it is no longer assigned to any slots, then we remove the action
// altogether.
if (Action.Temporary)
{
// Theres probably a better way to do this.....
DebugTools.Assert(Action.ClientExclusive, "Temporary-actions must be client exclusive");
if (!_actionsUI.System.Assignments.Assignments.TryGetValue(Action, out var index)
|| index.Count == 0)
{
_actionsUI.Component.Actions.Remove(Action);
}
}
_actionsUI.StopTargeting();
_actionsUI.UpdateUI();
}
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 = _actionsUI.System.Assignments[_actionsUI.SelectedHotbar, fromIdx];
var toIdx = targetSlot.SlotIndex;
var toAssignment = _actionsUI.System.Assignments[_actionsUI.SelectedHotbar, toIdx];
if (fromIdx == toIdx) return;
if (fromAssignment == null) return;
_actionsUI.System.Assignments.AssignSlot(_actionsUI.SelectedHotbar, toIdx, fromAssignment);
if (toAssignment != null)
{
_actionsUI.System.Assignments.AssignSlot(_actionsUI.SelectedHotbar, fromIdx, toAssignment);
}
else
{
_actionsUI.System.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.
/// </summary>
public void Depress(bool depress)
{
// action can still be toggled if it's allowed to stay selected
if (Action == null || !Action.Enabled) return;
if (_depressed && !depress)
{
// fire the action
_actionsUI.System.OnSlotPressed(this);
}
_depressed = depress;
}
/// <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>
public void Assign(ActionType action)
{
// already assigned
if (Action != null && Action == action) return;
Action = action;
HideTooltip();
UpdateIcons();
DrawModeChanged();
_number.SetMessage(SlotNumberLabel());
}
/// <summary>
/// Clears the action assigned to this slot
/// </summary>
public void Clear()
{
if (Action == null) return;
Action = null;
_depressed = false;
HideTooltip();
UpdateIcons();
DrawModeChanged();
_number.SetMessage(SlotNumberLabel());
}
/// <summary>
/// Display the action in this slot (if there is one) as enabled
/// </summary>
public void Enable()
{
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 Disable()
{
_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 = (Action == null || Action.Enabled) ? EnabledColor : DisabledColor;
return FormattedMessage.FromMarkup("[color=" + color + "]" + number + "[/color]");
}
public void UpdateIcons()
{
UpdateItemIcon();
if (Action == null)
{
SetActionIcon(null);
return;
}
if ((_actionsUI.SelectingTargetFor?.Action == Action || Action.Toggled) && Action.IconOn != null)
SetActionIcon(Action.IconOn.Frame0());
else
SetActionIcon(Action.Icon?.Frame0());
}
private void SetActionIcon(Texture? texture)
{
if (texture == null || Action == null)
{
_bigActionIcon.Texture = null;
_bigActionIcon.Visible = false;
_smallActionIcon.Texture = null;
_smallActionIcon.Visible = false;
}
else if (Action.EntityIcon != null && Action.ItemIconStyle == ItemActionIconStyle.BigItem)
{
_smallActionIcon.Texture = texture;
_smallActionIcon.Modulate = Action.IconColor;
_smallActionIcon.Visible = true;
_bigActionIcon.Texture = null;
_bigActionIcon.Visible = false;
}
else
{
_bigActionIcon.Texture = texture;
_bigActionIcon.Modulate = Action.IconColor;
_bigActionIcon.Visible = true;
_smallActionIcon.Texture = null;
_smallActionIcon.Visible = false;
}
}
private void UpdateItemIcon()
{
if (Action?.EntityIcon != null && !_entMan.EntityExists(Action.EntityIcon))
{
// This is almost certainly because a player received/processed their own actions component state before
// being send the entity in their inventory that enabled this action.
// Defer updating icons to the next FrameUpdate().
_spriteViewDirty = true;
return;
}
if (Action?.EntityIcon == null || !_entMan.TryGetComponent(Action.EntityIcon.Value, out SpriteComponent? sprite))
{
_bigItemSpriteView.Visible = false;
_bigItemSpriteView.Sprite = null;
_smallItemSpriteView.Visible = false;
_smallItemSpriteView.Sprite = null;
}
else
{
switch (Action.ItemIconStyle)
{
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;
}
}
}
public void DrawModeChanged()
{
// always show the normal empty button style if no action in this slot
if (Action == null)
{
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassNormal);
return;
}
// show a hover only if the action is usable or another action is being dragged on top of this
if (_beingHovered && (_actionsUI.DragDropHelper.IsDragging || _actionMenu.IsDragging || Action.Enabled))
{
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassHover);
}
// 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 (Action.Toggled || _actionsUI.SelectingTargetFor == this)
{
// when there's a toggle sprite, we're showing that sprite instead of highlighting this slot
SetOnlyStylePseudoClass(Action.IconOn != null ? ContainerButton.StylePseudoClassNormal :
ContainerButton.StylePseudoClassPressed);
return;
}
if (!Action.Enabled)
{
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassDisabled);
return;
}
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassNormal);
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
if (_spriteViewDirty)
{
_spriteViewDirty = false;
UpdateIcons();
}
if (Action == null || Action.Cooldown == null || !Action.Enabled)
{
_cooldownGraphic.Visible = false;
_cooldownGraphic.Progress = 0;
return;
}
var cooldown = Action.Cooldown.Value;
var duration = cooldown.End - cooldown.Start;
var curTime = _gameTiming.CurTime;
var length = duration.TotalSeconds;
var progress = (curTime - cooldown.Start).TotalSeconds / length;
var ratio = (progress <= 1 ? (1 - progress) : (curTime - cooldown.End).TotalSeconds * -5);
_cooldownGraphic.Progress = MathHelper.Clamp((float)ratio, -1, 1);
if (ratio > -1f)
_cooldownGraphic.Visible = true;
else
{
_cooldownGraphic.Visible = false;
Action.Cooldown = null;
DrawModeChanged();
}
}
}
}

View File

@@ -1,505 +0,0 @@
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;
private readonly IEntityManager _entMan;
private readonly IGameTiming _timing;
/// <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>();
_timing = IoCManager.Resolve<IGameTiming>();
_entMan = IoCManager.Resolve<IEntityManager>();
_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, _timing, _entMan);
_slotContainer.AddChild(slot);
_slots[i] = slot;
}
DragDropHelper = new DragDropHelper<ActionSlot>(OnBeginActionDrag, OnContinueActionDrag, OnEndActionDrag);
DragDropHelper.Deadzone = 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);
}
}
}