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
This commit is contained in:
11
Content.Client/Actions/ActionEvents.cs
Normal file
11
Content.Client/Actions/ActionEvents.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Content.Shared.Actions.ActionTypes;
|
||||
|
||||
namespace Content.Client.Actions;
|
||||
|
||||
/// <summary>
|
||||
/// This event is raised when a user clicks on an empty action slot. Enables other systems to fill this slow.
|
||||
/// </summary>
|
||||
public sealed class FillActionSlotEvent : EntityEventArgs
|
||||
{
|
||||
public ActionType? Action;
|
||||
}
|
||||
@@ -1,20 +1,64 @@
|
||||
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 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.Shared.GameObjects;
|
||||
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.IoC;
|
||||
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
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public sealed class ActionsSystem : EntitySystem
|
||||
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!;
|
||||
|
||||
// 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 const byte Hotbars = 9;
|
||||
public const byte Slots = 10;
|
||||
|
||||
public bool UIDirty;
|
||||
|
||||
public ActionsUI? Ui;
|
||||
private EntityUid? _highlightedEntity;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -64,12 +108,207 @@ namespace Content.Client.Actions
|
||||
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),
|
||||
.BindBefore(EngineKeyFunctions.Use, new PointerInputCmdHandler(TargetingOnUse, outsidePrediction: true),
|
||||
typeof(ConstructionSystem), typeof(DragDropSystem))
|
||||
.BindBefore(EngineKeyFunctions.UIRightClick, new PointerInputCmdHandler(TargetingCancel, outsidePrediction: true))
|
||||
.Register<ActionsSystem>();
|
||||
|
||||
SubscribeLocalEvent<ClientActionsComponent, PlayerAttachedEvent>((_, component, _) => component.PlayerAttached());
|
||||
SubscribeLocalEvent<ClientActionsComponent, PlayerDetachedEvent>((_, component, _) => component.PlayerDetached());
|
||||
SubscribeLocalEvent<ActionsComponent, PlayerAttachedEvent>(OnPlayerAttached);
|
||||
SubscribeLocalEvent<ActionsComponent, PlayerDetachedEvent>(OnPlayerDetached);
|
||||
SubscribeLocalEvent<ActionsComponent, ComponentHandleState>(HandleState);
|
||||
}
|
||||
|
||||
protected override void Dirty(ActionType action)
|
||||
{
|
||||
// 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)
|
||||
return;
|
||||
|
||||
base.Dirty(action);
|
||||
UIDirty = true;
|
||||
}
|
||||
|
||||
private void HandleState(EntityUid uid, ActionsComponent component, ref ComponentHandleState args)
|
||||
{
|
||||
// Client only needs to care about local player.
|
||||
if (uid != _playerManager.LocalPlayer?.ControlledEntity)
|
||||
return;
|
||||
|
||||
if (args.Current is not ActionsComponentState state)
|
||||
return;
|
||||
|
||||
var serverActions = new SortedSet<ActionType>(state.Actions);
|
||||
|
||||
foreach (var act in component.Actions.ToList())
|
||||
{
|
||||
if (act.ClientExclusive)
|
||||
continue;
|
||||
|
||||
if (!serverActions.TryGetValue(act, out var serverAct))
|
||||
{
|
||||
component.Actions.Remove(act);
|
||||
if (act.AutoRemove && !(Ui?.Locked ?? false))
|
||||
Assignments.Remove(act);
|
||||
continue;
|
||||
}
|
||||
|
||||
act.CopyFrom(serverAct);
|
||||
serverActions.Remove(serverAct);
|
||||
|
||||
if (act is EntityTargetAction entAct)
|
||||
{
|
||||
entAct.Whitelist?.UpdateRegistrations();
|
||||
}
|
||||
}
|
||||
|
||||
// Anything that remains is a new action
|
||||
foreach (var newAct in serverActions)
|
||||
{
|
||||
if (newAct is EntityTargetAction entAct)
|
||||
entAct.Whitelist?.UpdateRegistrations();
|
||||
|
||||
// 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 (uid != _playerManager.LocalPlayer?.ControlledEntity)
|
||||
return;
|
||||
|
||||
if (!Resolve(uid, ref comp, false))
|
||||
return;
|
||||
|
||||
base.AddAction(uid, action, provider, comp, dirty);
|
||||
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)
|
||||
{
|
||||
Assignments.Assignments.Remove(action);
|
||||
continue;
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private void OnPlayerAttached(EntityUid uid, ActionsComponent component, PlayerAttachedEvent args)
|
||||
{
|
||||
Assignments = new(Hotbars, Slots);
|
||||
Ui = new ActionsUI(this, component);
|
||||
_uiManager.StateRoot.AddChild(Ui);
|
||||
UIDirty = true;
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
@@ -85,9 +324,9 @@ namespace Content.Client.Actions
|
||||
{
|
||||
var playerEntity = _playerManager.LocalPlayer?.ControlledEntity;
|
||||
if (playerEntity == null ||
|
||||
!EntityManager.TryGetComponent<ClientActionsComponent?>(playerEntity.Value, out var actionsComponent)) return false;
|
||||
!EntityManager.TryGetComponent<ActionsComponent?>(playerEntity.Value, out var actionsComponent)) return false;
|
||||
|
||||
actionsComponent.HandleHotbarKeybind(slot, args);
|
||||
HandleHotbarKeybind(slot, args);
|
||||
return true;
|
||||
}, false);
|
||||
}
|
||||
@@ -98,28 +337,363 @@ namespace Content.Client.Actions
|
||||
return new((in PointerInputCmdHandler.PointerInputCmdArgs args) =>
|
||||
{
|
||||
var playerEntity = _playerManager.LocalPlayer?.ControlledEntity;
|
||||
if (!EntityManager.TryGetComponent<ClientActionsComponent?>(playerEntity, out var actionsComponent)) return false;
|
||||
if (!EntityManager.TryGetComponent<ActionsComponent?>(playerEntity, out var actionsComponent)) return false;
|
||||
|
||||
actionsComponent.HandleChangeHotbarKeybind(hotbar, args);
|
||||
HandleChangeHotbarKeybind(hotbar, args);
|
||||
return true;
|
||||
},
|
||||
false);
|
||||
}
|
||||
|
||||
private bool TargetingOnUse(in PointerInputCmdHandler.PointerInputCmdArgs args)
|
||||
{
|
||||
var playerEntity = _playerManager.LocalPlayer?.ControlledEntity;
|
||||
if (!EntityManager.TryGetComponent<ClientActionsComponent?>(playerEntity, out var actionsComponent)) return false;
|
||||
|
||||
return actionsComponent.TargetingOnUse(args);
|
||||
}
|
||||
|
||||
private void ToggleActionsMenu()
|
||||
{
|
||||
var playerEntity = _playerManager.LocalPlayer?.ControlledEntity;
|
||||
if (!EntityManager.TryGetComponent<ClientActionsComponent?>(playerEntity, out var actionsComponent)) return;
|
||||
Ui?.ToggleActionsMenu();
|
||||
}
|
||||
|
||||
actionsComponent.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)
|
||||
return;
|
||||
|
||||
if (slot.Action == null || _playerManager.LocalPlayer?.ControlledEntity is not EntityUid user)
|
||||
return;
|
||||
|
||||
if (slot.Action.Provider != null && Deleted(slot.Action.Provider))
|
||||
return;
|
||||
|
||||
if (slot.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 (instantAction.Event != null)
|
||||
instantAction.Event.Performer = user;
|
||||
|
||||
PerformAction(Ui.Component, instantAction, instantAction.Event, GameTiming.CurTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
var request = new RequestPerformActionEvent(instantAction);
|
||||
EntityManager.RaisePredictiveEvent(request);
|
||||
}
|
||||
}
|
||||
|
||||
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(Filter.Local(), action.Sound.GetSound(), 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;
|
||||
|
||||
Ui.Component.Actions.Add(fillEvent.Action);
|
||||
Assignments.AssignSlot(hotbar, index, fillEvent.Action);
|
||||
|
||||
Ui.UpdateUI();
|
||||
}
|
||||
|
||||
public void SaveActionAssignments(string path)
|
||||
{
|
||||
// Disabled until YamlMappingFix's sandbox issues are resolved.
|
||||
|
||||
/*
|
||||
// Currently only tested with temporary innate actions (i.e., mapping actions). No guarantee it works with
|
||||
// other actions. If its meant to be used for full game state saving/loading, the entity that provides
|
||||
// actions needs to keep the same uid.
|
||||
|
||||
var sequence = new SequenceDataNode();
|
||||
|
||||
foreach (var (action, assigns) in Assignments.Assignments)
|
||||
{
|
||||
var slot = new MappingDataNode();
|
||||
slot.Add("action", _serializationManager.WriteValue(action));
|
||||
slot.Add("assignments", _serializationManager.WriteValue(assigns));
|
||||
sequence.Add(slot);
|
||||
}
|
||||
|
||||
using var writer = _resourceManager.UserData.OpenWriteText(new ResourcePath(path).ToRootedPath());
|
||||
var stream = new YamlStream { new(sequence.ToSequenceNode()) };
|
||||
stream.Save(new YamlMappingFix(new Emitter(writer)), false);
|
||||
*/
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load actions and their toolbar assignments from a file.
|
||||
/// </summary>
|
||||
public void LoadActionAssignments(string path, bool userData)
|
||||
{
|
||||
if (Ui == null)
|
||||
return;
|
||||
|
||||
var file = new ResourcePath(path).ToRootedPath();
|
||||
TextReader reader = userData
|
||||
? _resourceManager.UserData.OpenText(file)
|
||||
: _resourceManager.ContentFileReadText(file);
|
||||
|
||||
var yamlStream = new YamlStream();
|
||||
yamlStream.Load(reader);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var entry in sequence.Sequence)
|
||||
{
|
||||
if (entry is not MappingDataNode map)
|
||||
continue;
|
||||
|
||||
if (!map.TryGet("action", out var actionNode))
|
||||
continue;
|
||||
|
||||
var action = _serializationManager.ReadValueCast<ActionType>(typeof(ActionType), actionNode);
|
||||
if (action == null)
|
||||
continue;
|
||||
|
||||
if (Ui.Component.Actions.TryGetValue(action, out var existingAction))
|
||||
{
|
||||
existingAction.CopyFrom(action);
|
||||
action = existingAction;
|
||||
}
|
||||
else
|
||||
Ui.Component.Actions.Add(action);
|
||||
|
||||
if (!map.TryGet("assignments", out var assignmentNode))
|
||||
continue;
|
||||
|
||||
var assignments = _serializationManager.ReadValueCast<List<(byte Hotbar, byte Slot)>>(typeof(List<(byte Hotbar, byte Slot)>), assignmentNode);
|
||||
if (assignments == null)
|
||||
continue;
|
||||
|
||||
foreach (var index in assignments)
|
||||
{
|
||||
Assignments.AssignSlot(index.Hotbar, index.Slot, action);
|
||||
}
|
||||
}
|
||||
|
||||
UIDirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
using System;
|
||||
using Content.Shared.Actions;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Client.Actions.Assignments
|
||||
{
|
||||
public struct ActionAssignment : IEquatable<ActionAssignment>
|
||||
{
|
||||
private readonly ActionType _actionType;
|
||||
private readonly ItemActionType _itemActionType;
|
||||
private readonly EntityUid _item;
|
||||
public Assignment Assignment { get; private init; }
|
||||
|
||||
private ActionAssignment(Assignment assignment, ActionType actionType, ItemActionType itemActionType, EntityUid item)
|
||||
{
|
||||
Assignment = assignment;
|
||||
_actionType = actionType;
|
||||
_itemActionType = itemActionType;
|
||||
_item = item;
|
||||
}
|
||||
|
||||
/// <param name="actionType">the action type, if our Assignment is Assignment.Action</param>
|
||||
/// <returns>true only if our Assignment is Assignment.Action</returns>
|
||||
public bool TryGetAction(out ActionType actionType)
|
||||
{
|
||||
actionType = _actionType;
|
||||
return Assignment == Assignment.Action;
|
||||
}
|
||||
|
||||
/// <param name="itemActionType">the item action type, if our Assignment is Assignment.ItemActionWithoutItem</param>
|
||||
/// <returns>true only if our Assignment is Assignment.ItemActionWithoutItem</returns>
|
||||
public bool TryGetItemActionWithoutItem(out ItemActionType itemActionType)
|
||||
{
|
||||
itemActionType = _itemActionType;
|
||||
return Assignment == Assignment.ItemActionWithoutItem;
|
||||
}
|
||||
|
||||
/// <param name="itemActionType">the item action type, if our Assignment is Assignment.ItemActionWithItem</param>
|
||||
/// <param name="item">the item UID providing the action, if our Assignment is Assignment.ItemActionWithItem</param>
|
||||
/// <returns>true only if our Assignment is Assignment.ItemActionWithItem</returns>
|
||||
public bool TryGetItemActionWithItem(out ItemActionType itemActionType, out EntityUid item)
|
||||
{
|
||||
itemActionType = _itemActionType;
|
||||
item = _item;
|
||||
return Assignment == Assignment.ItemActionWithItem;
|
||||
}
|
||||
|
||||
public static ActionAssignment For(ActionType actionType)
|
||||
{
|
||||
return new(Assignment.Action, actionType, default, default);
|
||||
}
|
||||
|
||||
public static ActionAssignment For(ItemActionType actionType)
|
||||
{
|
||||
return new(Assignment.ItemActionWithoutItem, default, actionType, default);
|
||||
}
|
||||
|
||||
public static ActionAssignment For(ItemActionType actionType, EntityUid item)
|
||||
{
|
||||
return new(Assignment.ItemActionWithItem, default, actionType, item);
|
||||
}
|
||||
|
||||
public bool Equals(ActionAssignment other)
|
||||
{
|
||||
return _actionType == other._actionType && _itemActionType == other._itemActionType && Equals(_item, other._item);
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is ActionAssignment other && Equals(other);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(_actionType, _itemActionType, _item);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{nameof(_actionType)}: {_actionType}, {nameof(_itemActionType)}: {_itemActionType}, {nameof(_item)}: {_item}, {nameof(Assignment)}: {Assignment}";
|
||||
}
|
||||
}
|
||||
|
||||
public enum Assignment : byte
|
||||
{
|
||||
Action,
|
||||
ItemActionWithoutItem,
|
||||
ItemActionWithItem
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Actions.Components;
|
||||
using Robust.Shared.GameObjects;
|
||||
using System.Collections.Generic;
|
||||
using Content.Shared.Actions.ActionTypes;
|
||||
using System.Linq;
|
||||
|
||||
namespace Content.Client.Actions.Assignments
|
||||
@@ -9,6 +7,7 @@ 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),
|
||||
@@ -19,7 +18,7 @@ namespace Content.Client.Actions.Assignments
|
||||
/// 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 ActionAssignment?[,] _slots;
|
||||
private readonly ActionType?[,] _slots;
|
||||
|
||||
/// <summary>
|
||||
/// Hotbar and slot assignment for each action type (slot index 0 corresponds to the one labeled "1",
|
||||
@@ -28,14 +27,14 @@ namespace Content.Client.Actions.Assignments
|
||||
/// 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>
|
||||
private readonly Dictionary<ActionAssignment, List<(byte Hotbar, byte Slot)>> _assignments;
|
||||
[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>
|
||||
private readonly HashSet<ActionType> _preventAutoPopulate = new();
|
||||
private readonly Dictionary<EntityUid, HashSet<ItemActionType>> _preventAutoPopulateItem = new();
|
||||
public readonly SortedSet<ActionType> PreventAutoPopulate = new();
|
||||
|
||||
private readonly byte _numHotbars;
|
||||
private readonly byte _numSlots;
|
||||
@@ -44,105 +43,25 @@ namespace Content.Client.Actions.Assignments
|
||||
{
|
||||
_numHotbars = numHotbars;
|
||||
_numSlots = numSlots;
|
||||
_assignments = new Dictionary<ActionAssignment, List<(byte Hotbar, byte Slot)>>();
|
||||
_slots = new ActionAssignment?[numHotbars, numSlots];
|
||||
_slots = new ActionType?[numHotbars, numSlots];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the assignments based on the current states of all the actions.
|
||||
/// Newly-granted actions or item actions which don't have an assignment will be assigned a slot
|
||||
/// automatically (unless they've been manually cleared). Item-based actions
|
||||
/// which no longer have an associated state will be decoupled from their item.
|
||||
/// </summary>
|
||||
public void Reconcile(byte currentHotbar, IReadOnlyDictionary<ActionType, ActionState> actionStates,
|
||||
IReadOnlyDictionary<EntityUid, Dictionary<ItemActionType, ActionState>> itemActionStates,
|
||||
bool actionMenuLocked)
|
||||
public bool Remove(ActionType action) => Replace(action, null);
|
||||
|
||||
internal bool Replace(ActionType action, ActionType? newAction)
|
||||
{
|
||||
// if we've been granted any actions which have no assignment to any hotbar, we must auto-populate them
|
||||
// into the hotbar so the user knows about them.
|
||||
// We fill their current hotbar first, rolling over to the next open slot on the next hotbar.
|
||||
foreach (var actionState in actionStates)
|
||||
if (!Assignments.Remove(action, out var assigns))
|
||||
return false;
|
||||
|
||||
if (newAction != null)
|
||||
Assignments[newAction] = assigns;
|
||||
|
||||
foreach (var (bar, slot) in assigns)
|
||||
{
|
||||
var assignment = ActionAssignment.For(actionState.Key);
|
||||
if (actionState.Value.Enabled && !_assignments.ContainsKey(assignment))
|
||||
{
|
||||
AutoPopulate(assignment, currentHotbar, false);
|
||||
}
|
||||
_slots[bar, slot] = newAction;
|
||||
}
|
||||
|
||||
foreach (var (item, itemStates) in itemActionStates)
|
||||
{
|
||||
foreach (var itemActionState in itemStates)
|
||||
{
|
||||
// unlike regular actions, we DO actually show user their new item action even when it's disabled.
|
||||
// this allows them to instantly see when an action may be possible that is provided by an item but
|
||||
// something is preventing it
|
||||
// Note that we are checking if there is an explicit assignment for this item action + item,
|
||||
// we will determine during auto-population if we should tie the item to an existing "item action only"
|
||||
// assignment
|
||||
var assignment = ActionAssignment.For(itemActionState.Key, item);
|
||||
if (!_assignments.ContainsKey(assignment))
|
||||
{
|
||||
AutoPopulate(assignment, currentHotbar, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We need to figure out which current item action assignments we had
|
||||
// which once had an associated item but have been revoked (based on our newly provided action states)
|
||||
// so we can dissociate them from the item. If the provided action states do not
|
||||
// have a state for this action type + item, we can assume that the action has been revoked for that item.
|
||||
var assignmentsWithoutItem = new List<KeyValuePair<ActionAssignment, List<(byte Hotbar, byte Slot)>>>();
|
||||
foreach (var assignmentEntry in _assignments)
|
||||
{
|
||||
if (!assignmentEntry.Key.TryGetItemActionWithItem(out var actionType, out var item))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// we have this assignment currently tied to an item,
|
||||
// check if it no longer has an associated item in our dict of states
|
||||
if (itemActionStates.TryGetValue(item, out var states))
|
||||
{
|
||||
if (states.ContainsKey(actionType))
|
||||
{
|
||||
// we have a state for this item + action type so we won't
|
||||
// remove the item from the assignment
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
assignmentsWithoutItem.Add(assignmentEntry);
|
||||
}
|
||||
|
||||
// reassign without the item for each assignment we found that no longer has an associated item
|
||||
foreach (var (assignment, slots) in assignmentsWithoutItem)
|
||||
{
|
||||
foreach (var (hotbar, slot) in slots)
|
||||
{
|
||||
if (!assignment.TryGetItemActionWithItem(out var actionType, out _))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (actionMenuLocked)
|
||||
{
|
||||
AssignSlot(hotbar, slot, ActionAssignment.For(actionType));
|
||||
}
|
||||
else
|
||||
{
|
||||
ClearSlot(hotbar, slot, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Additionally, we must find items which have no action states at all in our newly provided states so
|
||||
// we can assume their item was unequipped and reset them to allow auto-population.
|
||||
var itemsWithoutState = _preventAutoPopulateItem.Keys.Where(item => !itemActionStates.ContainsKey(item));
|
||||
foreach (var toRemove in itemsWithoutState)
|
||||
{
|
||||
_preventAutoPopulateItem.Remove(toRemove);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -151,18 +70,18 @@ namespace Content.Client.Actions.Assignments
|
||||
/// <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, ActionAssignment actionType)
|
||||
public void AssignSlot(byte hotbar, byte slot, ActionType actionType)
|
||||
{
|
||||
ClearSlot(hotbar, slot, false);
|
||||
_slots[hotbar, slot] = actionType;
|
||||
if (_assignments.TryGetValue(actionType, out var slotList))
|
||||
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;
|
||||
Assignments[actionType] = newList;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,40 +102,21 @@ namespace Content.Client.Actions.Assignments
|
||||
// (keeping in mind something can be assigned multiple slots)
|
||||
var currentAction = _slots[hotbar, slot];
|
||||
|
||||
if (!currentAction.HasValue)
|
||||
{
|
||||
if (currentAction == null)
|
||||
return;
|
||||
}
|
||||
|
||||
if (preventAutoPopulate)
|
||||
{
|
||||
var assignment = currentAction.Value;
|
||||
PreventAutoPopulate.Add(currentAction);
|
||||
|
||||
if (assignment.TryGetAction(out var actionType))
|
||||
{
|
||||
_preventAutoPopulate.Add(actionType);
|
||||
}
|
||||
else if (assignment.TryGetItemActionWithItem(out var itemActionType, out var item))
|
||||
{
|
||||
if (!_preventAutoPopulateItem.TryGetValue(item, out var actionTypes))
|
||||
{
|
||||
actionTypes = new HashSet<ItemActionType>();
|
||||
_preventAutoPopulateItem[item] = actionTypes;
|
||||
}
|
||||
|
||||
actionTypes.Add(itemActionType);
|
||||
}
|
||||
}
|
||||
|
||||
var assignmentList = _assignments[currentAction.Value];
|
||||
var assignmentList = Assignments[currentAction];
|
||||
assignmentList = assignmentList.Where(a => a.Hotbar != hotbar || a.Slot != slot).ToList();
|
||||
if (!assignmentList.Any())
|
||||
{
|
||||
_assignments.Remove(currentAction.Value);
|
||||
Assignments.Remove(currentAction);
|
||||
}
|
||||
else
|
||||
{
|
||||
_assignments[currentAction.Value] = assignmentList;
|
||||
Assignments[currentAction] = assignmentList;
|
||||
}
|
||||
|
||||
_slots[hotbar, slot] = null;
|
||||
@@ -231,45 +131,10 @@ namespace Content.Client.Actions.Assignments
|
||||
/// 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(ActionAssignment toAssign, byte currentHotbar, bool force = true)
|
||||
public void AutoPopulate(ActionType toAssign, byte currentHotbar, bool force = true)
|
||||
{
|
||||
if (ShouldPreventAutoPopulate(toAssign, force))
|
||||
{
|
||||
if (!force && PreventAutoPopulate.Contains(toAssign))
|
||||
return;
|
||||
}
|
||||
|
||||
// if the assignment to make is an item action with an associated item,
|
||||
// then first look for currently assigned item actions without an item, to replace with this
|
||||
// assignment
|
||||
if (toAssign.TryGetItemActionWithItem(out var actionType, out var _))
|
||||
{
|
||||
if (_assignments.TryGetValue(ActionAssignment.For(actionType),
|
||||
out var possibilities))
|
||||
{
|
||||
// use the closest assignment to current hotbar
|
||||
byte hotbar = 0;
|
||||
byte slot = 0;
|
||||
var minCost = int.MaxValue;
|
||||
foreach (var possibility in possibilities)
|
||||
{
|
||||
var cost = possibility.Slot + _numSlots * (currentHotbar >= possibility.Hotbar
|
||||
? currentHotbar - possibility.Hotbar
|
||||
: _numHotbars - currentHotbar + possibility.Hotbar);
|
||||
if (cost < minCost)
|
||||
{
|
||||
hotbar = possibility.Hotbar;
|
||||
slot = possibility.Slot;
|
||||
minCost = cost;
|
||||
}
|
||||
}
|
||||
|
||||
if (minCost != int.MaxValue)
|
||||
{
|
||||
AssignSlot(hotbar, slot, toAssign);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (byte hotbarOffset = 0; hotbarOffset < _numHotbars; hotbarOffset++)
|
||||
{
|
||||
@@ -277,21 +142,10 @@ namespace Content.Client.Actions.Assignments
|
||||
{
|
||||
var hotbar = (byte) ((currentHotbar + hotbarOffset) % _numHotbars);
|
||||
var slotAssignment = _slots[hotbar, slot];
|
||||
if (slotAssignment.HasValue)
|
||||
{
|
||||
// if the assignment in this slot is an item action without an associated item,
|
||||
// then tie it to the current item if we are trying to auto populate an item action.
|
||||
if (toAssign.Assignment == Assignment.ItemActionWithItem &&
|
||||
slotAssignment.Value.Assignment == Assignment.ItemActionWithoutItem)
|
||||
{
|
||||
AssignSlot(hotbar, slot, toAssign);
|
||||
return;
|
||||
}
|
||||
|
||||
if (slotAssignment != null)
|
||||
continue;
|
||||
}
|
||||
|
||||
// slot's empty, assign
|
||||
AssignSlot(hotbar, slot, toAssign);
|
||||
return;
|
||||
}
|
||||
@@ -299,36 +153,15 @@ namespace Content.Client.Actions.Assignments
|
||||
// there was no empty slot
|
||||
}
|
||||
|
||||
private bool ShouldPreventAutoPopulate(ActionAssignment assignment, bool force)
|
||||
{
|
||||
if (force)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (assignment.TryGetAction(out var actionType))
|
||||
{
|
||||
return _preventAutoPopulate.Contains(actionType);
|
||||
}
|
||||
|
||||
if (assignment.TryGetItemActionWithItem(out var itemActionType, out var item))
|
||||
{
|
||||
return _preventAutoPopulateItem.TryGetValue(item,
|
||||
out var itemActionTypes) && itemActionTypes.Contains(itemActionType);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the assignment to the indicated slot if there is one.
|
||||
/// </summary>
|
||||
public ActionAssignment? this[in byte hotbar, in byte slot] => _slots[hotbar, slot];
|
||||
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(ActionAssignment assignment)
|
||||
public bool HasAssignment(ActionType assignment)
|
||||
{
|
||||
return _assignments.ContainsKey(assignment);
|
||||
return Assignments.ContainsKey(assignment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,252 +0,0 @@
|
||||
using Content.Client.Actions.Assignments;
|
||||
using Content.Client.Actions.UI;
|
||||
using Content.Client.Items.Managers;
|
||||
using Content.Shared.Actions.Components;
|
||||
using Content.Shared.Actions.Prototypes;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Input.Binding;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Client.Actions
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(SharedActionsComponent))]
|
||||
public sealed class ClientActionsComponent : SharedActionsComponent
|
||||
{
|
||||
public const byte Hotbars = 9;
|
||||
public const byte Slots = 10;
|
||||
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IItemSlotManager _itemSlotManager = default!;
|
||||
|
||||
private ActionsUI? _ui;
|
||||
private EntityUid _highlightedEntity;
|
||||
|
||||
/// <summary>
|
||||
/// Current assignments for all hotbars / slots for this entity.
|
||||
/// </summary>
|
||||
public ActionAssignments Assignments { get; } = new(Hotbars, Slots);
|
||||
|
||||
/// <summary>
|
||||
/// Allows calculating if we need to act due to this component being controlled by the current mob
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
private bool CurrentlyControlled => _playerManager.LocalPlayer != null && _playerManager.LocalPlayer.ControlledEntity == Owner;
|
||||
|
||||
protected override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
PlayerDetached();
|
||||
}
|
||||
|
||||
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
|
||||
{
|
||||
base.HandleComponentState(curState, nextState);
|
||||
|
||||
if (curState is not ActionComponentState)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateUI();
|
||||
}
|
||||
|
||||
public void PlayerAttached()
|
||||
{
|
||||
if (!CurrentlyControlled || _ui != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_ui = new ActionsUI(this);
|
||||
IoCManager.Resolve<IUserInterfaceManager>().StateRoot.AddChild(_ui);
|
||||
UpdateUI();
|
||||
}
|
||||
|
||||
public void PlayerDetached()
|
||||
{
|
||||
if (_ui == null) return;
|
||||
IoCManager.Resolve<IUserInterfaceManager>().StateRoot.RemoveChild(_ui);
|
||||
_ui = null;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the displayed hotbar (and menu) based on current state of actions.
|
||||
/// </summary>
|
||||
private void UpdateUI()
|
||||
{
|
||||
if (!CurrentlyControlled || _ui == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Assignments.Reconcile(_ui.SelectedHotbar, ActionStates(), ItemActionStates(), _ui.Locked);
|
||||
|
||||
_ui.UpdateUI();
|
||||
}
|
||||
|
||||
public void AttemptAction(ActionSlot slot)
|
||||
{
|
||||
|
||||
var attempt = slot.ActionAttempt();
|
||||
if (attempt == null) return;
|
||||
|
||||
switch (attempt.Action.BehaviorType)
|
||||
{
|
||||
case BehaviorType.Instant:
|
||||
// for instant actions, we immediately tell the server we're doing it
|
||||
#pragma warning disable 618
|
||||
SendNetworkMessage(attempt.PerformInstantActionMessage());
|
||||
#pragma warning restore 618
|
||||
break;
|
||||
case BehaviorType.Toggle:
|
||||
// for toggle actions, we immediately tell the server we're toggling it.
|
||||
if (attempt.TryGetActionState(this, out var actionState))
|
||||
{
|
||||
// TODO: At the moment we always predict that the toggle will work clientside,
|
||||
// even if it sometimes may not (it will be reset by the server if wrong).
|
||||
attempt.ToggleAction(this, !actionState.ToggledOn);
|
||||
slot.ToggledOn = !actionState.ToggledOn;
|
||||
#pragma warning disable 618
|
||||
SendNetworkMessage(attempt.PerformToggleActionMessage(!actionState.ToggledOn));
|
||||
#pragma warning restore 618
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.ErrorS("action", "attempted to toggle action {0} which has" +
|
||||
" unknown state", attempt);
|
||||
}
|
||||
|
||||
break;
|
||||
case BehaviorType.TargetPoint:
|
||||
case BehaviorType.TargetEntity:
|
||||
// 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);
|
||||
break;
|
||||
case BehaviorType.None:
|
||||
break;
|
||||
default:
|
||||
Logger.ErrorS("action", "unhandled action press for action {0}",
|
||||
attempt);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles clicks when selecting the target for an action. Only has an effect when currently
|
||||
/// selecting a target.
|
||||
/// </summary>
|
||||
public bool TargetingOnUse(in PointerInputCmdHandler.PointerInputCmdArgs args)
|
||||
{
|
||||
// not currently predicted
|
||||
if (EntitySystem.Get<InputSystem>().Predicted) return false;
|
||||
|
||||
// only do something for actual target-based actions
|
||||
if (_ui?.SelectingTargetFor?.Action == null ||
|
||||
(!_ui.SelectingTargetFor.Action.IsTargetAction)) return false;
|
||||
|
||||
// do nothing if we know it's on cooldown
|
||||
if (_ui.SelectingTargetFor.IsOnCooldown) return false;
|
||||
|
||||
var attempt = _ui.SelectingTargetFor.ActionAttempt();
|
||||
if (attempt == null)
|
||||
{
|
||||
_ui.StopTargeting();
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (_ui.SelectingTargetFor.Action.BehaviorType)
|
||||
{
|
||||
case BehaviorType.TargetPoint:
|
||||
{
|
||||
// send our action to the server, we chose our target
|
||||
#pragma warning disable 618
|
||||
SendNetworkMessage(attempt.PerformTargetPointActionMessage(args));
|
||||
#pragma warning restore 618
|
||||
if (!attempt.Action.Repeat)
|
||||
{
|
||||
_ui.StopTargeting();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// target the currently hovered entity, if there is one
|
||||
case BehaviorType.TargetEntity when args.EntityUid != EntityUid.Invalid:
|
||||
{
|
||||
// send our action to the server, we chose our target
|
||||
#pragma warning disable 618
|
||||
SendNetworkMessage(attempt.PerformTargetEntityActionMessage(args));
|
||||
#pragma warning restore 618
|
||||
if (!attempt.Action.Repeat)
|
||||
{
|
||||
_ui.StopTargeting();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// we are supposed to target an entity but we didn't click it
|
||||
case BehaviorType.TargetEntity when args.EntityUid == EntityUid.Invalid:
|
||||
{
|
||||
if (attempt.Action.DeselectWhenEntityNotClicked)
|
||||
_ui.StopTargeting();
|
||||
return false;
|
||||
}
|
||||
default:
|
||||
_ui.StopTargeting();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void AfterActionChanged()
|
||||
{
|
||||
UpdateUI();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Highlights the item slot (inventory or hand) that contains this item
|
||||
/// </summary>
|
||||
/// <param name="item"></param>
|
||||
public void HighlightItemSlot(EntityUid item)
|
||||
{
|
||||
StopHighlightingItemSlots();
|
||||
|
||||
_highlightedEntity = item;
|
||||
_itemSlotManager.HighlightEntity(item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops highlighting any item slots we are currently highlighting.
|
||||
/// </summary>H
|
||||
public void StopHighlightingItemSlots()
|
||||
{
|
||||
if (_highlightedEntity == default)
|
||||
return;
|
||||
|
||||
_itemSlotManager.UnHighlightEntity(_highlightedEntity);
|
||||
_highlightedEntity = default;
|
||||
}
|
||||
|
||||
public void ToggleActionsMenu()
|
||||
{
|
||||
_ui?.ToggleActionsMenu();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Actions.ActionTypes;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Timing;
|
||||
@@ -89,7 +91,7 @@ namespace Content.Client.Actions.UI
|
||||
{
|
||||
var duration = Cooldown.Value.End - Cooldown.Value.Start;
|
||||
_cooldownLabel.SetMessage(FormattedMessage.FromMarkup(
|
||||
$"[color=#a10505]{duration.Seconds} sec cooldown ({timeLeft.Seconds + 1} sec remaining)[/color]"));
|
||||
$"[color=#a10505]{(int) duration.TotalSeconds} sec cooldown ({(int) timeLeft.TotalSeconds + 1} sec remaining)[/color]"));
|
||||
_cooldownLabel.Visible = true;
|
||||
}
|
||||
else
|
||||
|
||||
@@ -1,22 +1,16 @@
|
||||
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 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.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Timing;
|
||||
using static Robust.Client.UserInterface.Controls.BaseButton;
|
||||
using static Robust.Client.UserInterface.Controls.BoxContainer;
|
||||
@@ -29,28 +23,35 @@ namespace Content.Client.Actions.UI
|
||||
/// </summary>
|
||||
public sealed class ActionMenu : DefaultWindow
|
||||
{
|
||||
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";
|
||||
// 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);
|
||||
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;
|
||||
@@ -60,17 +61,16 @@ namespace Content.Client.Actions.UI
|
||||
private readonly TextureRect _dragShadow;
|
||||
private readonly IGameHud _gameHud;
|
||||
private readonly DragDropHelper<ActionMenuItem> _dragDropHelper;
|
||||
private readonly IEntityManager _entMan;
|
||||
|
||||
|
||||
public ActionMenu(ClientActionsComponent actionsComponent, ActionsUI actionsUI)
|
||||
public ActionMenu(ActionsUI actionsUI)
|
||||
{
|
||||
_actionsComponent = actionsComponent;
|
||||
_actionsUI = actionsUI;
|
||||
_actionManager = IoCManager.Resolve<ActionManager>();
|
||||
_gameHud = IoCManager.Resolve<IGameHud>();
|
||||
_entMan = IoCManager.Resolve<IEntityManager>();
|
||||
|
||||
Title = Loc.GetString("ui-actionmenu-title");
|
||||
MinSize = (300, 300);
|
||||
MinSize = (320, 300);
|
||||
|
||||
Contents.AddChild(new BoxContainer
|
||||
{
|
||||
@@ -115,27 +115,14 @@ namespace Content.Client.Actions.UI
|
||||
}
|
||||
});
|
||||
|
||||
// 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))
|
||||
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
|
||||
@@ -157,13 +144,6 @@ namespace Content.Client.Actions.UI
|
||||
_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()
|
||||
@@ -199,7 +179,7 @@ namespace Content.Client.Actions.UI
|
||||
|
||||
private bool OnBeginActionDrag()
|
||||
{
|
||||
_dragShadow.Texture = _dragDropHelper.Dragged!.Action.Icon.Frame0();
|
||||
_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;
|
||||
@@ -244,47 +224,7 @@ namespace Content.Client.Actions.UI
|
||||
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.System.Assignments.AssignSlot(_actionsUI.SelectedHotbar, targetSlot.SlotIndex, _dragDropHelper.Dragged.Action);
|
||||
_actionsUI.UpdateUI();
|
||||
}
|
||||
|
||||
@@ -300,19 +240,8 @@ namespace Content.Client.Actions.UI
|
||||
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.System.Assignments.AutoPopulate(actionMenuItem.Action, _actionsUI.SelectedHotbar);
|
||||
_actionsUI.UpdateUI();
|
||||
}
|
||||
|
||||
@@ -341,7 +270,7 @@ namespace Content.Client.Actions.UI
|
||||
return;
|
||||
}
|
||||
|
||||
var matchingActions = _actionManager.EnumerateActions()
|
||||
var matchingActions = _actionsUI.Component.Actions
|
||||
.Where(a => MatchesSearchCriteria(a, search, _filterButton.SelectedKeys));
|
||||
|
||||
PopulateActions(matchingActions);
|
||||
@@ -361,7 +290,7 @@ namespace Content.Client.Actions.UI
|
||||
}
|
||||
}
|
||||
|
||||
private bool MatchesSearchCriteria(BaseActionPrototype action, string standardizedSearch,
|
||||
private bool MatchesSearchCriteria(ActionType action, string standardizedSearch,
|
||||
IReadOnlyList<string> selectedFilterTags)
|
||||
{
|
||||
// check filter tag match first - each action must contain all filter tags currently selected.
|
||||
@@ -377,56 +306,32 @@ namespace Content.Client.Actions.UI
|
||||
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;
|
||||
// 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 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)
|
||||
private bool ActionMatchesFilterTag(ActionType 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)
|
||||
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,
|
||||
@@ -472,16 +377,15 @@ namespace Content.Client.Actions.UI
|
||||
return newText.ToString();
|
||||
}
|
||||
|
||||
private void PopulateActions(IEnumerable<BaseActionPrototype> actions)
|
||||
private void PopulateActions(IEnumerable<ActionType> actions)
|
||||
{
|
||||
ClearList();
|
||||
|
||||
_actionList = actions.ToArray();
|
||||
foreach (var action in _actionList.OrderBy(act => act.Name.ToString()))
|
||||
foreach (var action in actions)
|
||||
{
|
||||
var actionItem = new ActionMenuItem(action, OnItemFocusExited);
|
||||
var actionItem = new ActionMenuItem(_actionsUI, action, OnItemFocusExited);
|
||||
_resultsGrid.Children.Add(actionItem);
|
||||
actionItem.SetActionState(_actionsComponent.IsGranted(action));
|
||||
actionItem.SetActionState(action.Enabled);
|
||||
actionItem.OnButtonDown += OnItemButtonDown;
|
||||
actionItem.OnButtonUp += OnItemButtonUp;
|
||||
actionItem.OnPressed += OnItemPressed;
|
||||
@@ -496,7 +400,6 @@ namespace Content.Client.Actions.UI
|
||||
((ActionMenuItem) actionItem).OnPressed -= OnItemPressed;
|
||||
}
|
||||
_resultsGrid.Children.Clear();
|
||||
_actionList = EmptyActionList;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -508,8 +411,10 @@ namespace Content.Client.Actions.UI
|
||||
foreach (var actionItem in _resultsGrid.Children)
|
||||
{
|
||||
var actionMenuItem = ((ActionMenuItem) actionItem);
|
||||
actionMenuItem.SetActionState(_actionsComponent.IsGranted(actionMenuItem.Action));
|
||||
actionMenuItem.SetActionState(actionMenuItem.Action.Enabled);
|
||||
}
|
||||
|
||||
SearchAndDisplay();
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
using System;
|
||||
using System;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Shared.Actions.Prototypes;
|
||||
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>
|
||||
@@ -16,28 +24,165 @@ namespace Content.Client.Actions.UI
|
||||
// quickly explore what each action is
|
||||
private const float CustomTooltipDelay = 0.2f;
|
||||
|
||||
public BaseActionPrototype Action { get; private set; }
|
||||
private readonly TextureRect _bigActionIcon;
|
||||
private readonly TextureRect _smallActionIcon;
|
||||
private readonly SpriteView _smallItemSpriteView;
|
||||
private readonly SpriteView _bigItemSpriteView;
|
||||
|
||||
public ActionType Action;
|
||||
|
||||
private Action<ActionMenuItem> _onControlFocusExited;
|
||||
|
||||
public ActionMenuItem(BaseActionPrototype action, Action<ActionMenuItem> onControlFocusExited)
|
||||
private readonly ActionsUI _actionsUI;
|
||||
|
||||
public ActionMenuItem(ActionsUI actionsUI, ActionType action, Action<ActionMenuItem> onControlFocusExited)
|
||||
{
|
||||
_onControlFocusExited = onControlFocusExited;
|
||||
_actionsUI = actionsUI;
|
||||
Action = action;
|
||||
_onControlFocusExited = onControlFocusExited;
|
||||
|
||||
MinSize = (64, 64);
|
||||
VerticalAlignment = VAlignment.Top;
|
||||
|
||||
AddChild(new TextureRect
|
||||
_bigActionIcon = new TextureRect
|
||||
{
|
||||
HorizontalExpand = true,
|
||||
VerticalExpand = true,
|
||||
Stretch = TextureRect.StretchMode.Scale,
|
||||
Texture = action.Icon.Frame0()
|
||||
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.Provider != 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?.Provider == null || !IoCManager.Resolve<IEntityManager>().TryGetComponent(Action.Provider.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()
|
||||
@@ -48,7 +193,15 @@ namespace Content.Client.Actions.UI
|
||||
|
||||
private Control SupplyTooltip(Control? sender)
|
||||
{
|
||||
return new ActionAlertTooltip(Action.Name, Action.Description, Action.Requires);
|
||||
var name = FormattedMessage.FromMarkupPermissive(Loc.GetString(Action.Name));
|
||||
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>
|
||||
@@ -71,6 +224,5 @@ namespace Content.Client.Actions.UI
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,14 @@
|
||||
using System;
|
||||
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 Content.Shared.Inventory;
|
||||
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.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;
|
||||
using static Robust.Client.UserInterface.Controls.BoxContainer;
|
||||
@@ -37,48 +31,7 @@ namespace Content.Client.Actions.UI
|
||||
/// <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 EntityUid? 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();
|
||||
}
|
||||
}
|
||||
public ActionType? Action { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 1-10 corresponding to the number label on the slot (10 is labeled as 0)
|
||||
@@ -86,25 +39,6 @@ namespace Content.Client.Actions.UI
|
||||
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;
|
||||
@@ -114,8 +48,6 @@ namespace Content.Client.Actions.UI
|
||||
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;
|
||||
@@ -124,9 +56,8 @@ namespace Content.Client.Actions.UI
|
||||
/// 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)
|
||||
public ActionSlot(ActionsUI actionsUI, ActionMenu actionMenu, byte slotIndex)
|
||||
{
|
||||
_actionsComponent = actionsComponent;
|
||||
_actionsUI = actionsUI;
|
||||
_actionMenu = actionMenu;
|
||||
_gameTiming = IoCManager.Resolve<IGameTiming>();
|
||||
@@ -170,7 +101,8 @@ namespace Content.Client.Actions.UI
|
||||
{
|
||||
HorizontalAlignment = HAlignment.Right,
|
||||
VerticalAlignment = VAlignment.Bottom,
|
||||
Visible = false
|
||||
Visible = false,
|
||||
OverrideDirection = Direction.South,
|
||||
};
|
||||
|
||||
_cooldownGraphic = new CooldownGraphic {Progress = 0, Visible = false};
|
||||
@@ -219,24 +151,24 @@ namespace Content.Client.Actions.UI
|
||||
|
||||
private Control? SupplyTooltip(Control sender)
|
||||
{
|
||||
return Action == null ? null :
|
||||
new ActionAlertTooltip(Action.Name, Action.Description, Action.Requires) {Cooldown = Cooldown};
|
||||
}
|
||||
if (Action == null)
|
||||
return null;
|
||||
|
||||
/// <summary>
|
||||
/// Action attempt for performing the action in the slot
|
||||
/// </summary>
|
||||
public IActionAttempt? ActionAttempt()
|
||||
{
|
||||
IActionAttempt? attempt = Action switch
|
||||
string? extra = null;
|
||||
if (Action.Charges != null)
|
||||
{
|
||||
ActionPrototype actionPrototype => new ActionAttempt(actionPrototype),
|
||||
ItemActionPrototype itemActionPrototype =>
|
||||
Item.HasValue && IoCManager.Resolve<IEntityManager>().TryGetComponent<ItemActionsComponent?>(Item, out var itemActions) ?
|
||||
new ItemActionAttempt(itemActionPrototype, Item.Value, itemActions) : null,
|
||||
_ => null
|
||||
};
|
||||
return attempt;
|
||||
extra = Loc.GetString("ui-actionslot-charges", ("charges", Action.Charges));
|
||||
}
|
||||
|
||||
var name = FormattedMessage.FromMarkupPermissive(Loc.GetString(Action.Name));
|
||||
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()
|
||||
@@ -245,9 +177,9 @@ namespace Content.Client.Actions.UI
|
||||
|
||||
_beingHovered = true;
|
||||
DrawModeChanged();
|
||||
if (Action is not ItemActionPrototype) return;
|
||||
if (Item == null) return;
|
||||
_actionsComponent.HighlightItemSlot(Item.Value);
|
||||
|
||||
if (Action?.Provider != null)
|
||||
_actionsUI.System.HighlightItemSlot(Action.Provider.Value);
|
||||
}
|
||||
|
||||
protected override void MouseExited()
|
||||
@@ -256,32 +188,55 @@ namespace Content.Client.Actions.UI
|
||||
_beingHovered = false;
|
||||
CancelPress();
|
||||
DrawModeChanged();
|
||||
_actionsComponent.StopHighlightingItemSlots();
|
||||
_actionsUI.System.StopHighlightingItemSlot();
|
||||
}
|
||||
|
||||
protected override void KeyBindDown(GUIBoundKeyEventArgs args)
|
||||
{
|
||||
base.KeyBindDown(args);
|
||||
|
||||
if (args.Function == EngineKeyFunctions.UIRightClick)
|
||||
if (Action == null)
|
||||
{
|
||||
if (!_actionsUI.Locked && !_actionsUI.DragDropHelper.IsDragging && !_actionMenu.IsDragging)
|
||||
{
|
||||
_actionsComponent.Assignments.ClearSlot(_actionsUI.SelectedHotbar, SlotIndex, true);
|
||||
_actionsUI.StopTargeting();
|
||||
_actionsUI.UpdateUI();
|
||||
}
|
||||
// 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 || !HasAssignment)
|
||||
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;
|
||||
|
||||
// might turn into a drag or a full press if released
|
||||
Depress(true);
|
||||
_actionsUI.DragDropHelper.MouseDown(this);
|
||||
DrawModeChanged();
|
||||
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)
|
||||
@@ -299,21 +254,21 @@ namespace Content.Client.Actions.UI
|
||||
{
|
||||
// finish the drag, swap the 2 slots
|
||||
var fromIdx = SlotIndex;
|
||||
var fromAssignment = _actionsComponent.Assignments[_actionsUI.SelectedHotbar, fromIdx];
|
||||
var fromAssignment = _actionsUI.System.Assignments[_actionsUI.SelectedHotbar, fromIdx];
|
||||
var toIdx = targetSlot.SlotIndex;
|
||||
var toAssignment = _actionsComponent.Assignments[_actionsUI.SelectedHotbar, toIdx];
|
||||
var toAssignment = _actionsUI.System.Assignments[_actionsUI.SelectedHotbar, toIdx];
|
||||
|
||||
if (fromIdx == toIdx) return;
|
||||
if (!fromAssignment.HasValue) return;
|
||||
if (fromAssignment == null) return;
|
||||
|
||||
_actionsComponent.Assignments.AssignSlot(_actionsUI.SelectedHotbar, toIdx, fromAssignment.Value);
|
||||
if (toAssignment.HasValue)
|
||||
_actionsUI.System.Assignments.AssignSlot(_actionsUI.SelectedHotbar, toIdx, fromAssignment);
|
||||
if (toAssignment != null)
|
||||
{
|
||||
_actionsComponent.Assignments.AssignSlot(_actionsUI.SelectedHotbar, fromIdx, toAssignment.Value);
|
||||
_actionsUI.System.Assignments.AssignSlot(_actionsUI.SelectedHotbar, fromIdx, toAssignment);
|
||||
}
|
||||
else
|
||||
{
|
||||
_actionsComponent.Assignments.ClearSlot(_actionsUI.SelectedHotbar, fromIdx, false);
|
||||
_actionsUI.System.Assignments.ClearSlot(_actionsUI.SelectedHotbar, fromIdx, false);
|
||||
}
|
||||
_actionsUI.UpdateUI();
|
||||
}
|
||||
@@ -348,66 +303,19 @@ namespace Content.Client.Actions.UI
|
||||
|
||||
/// <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.
|
||||
/// trigger the action.
|
||||
/// </summary>
|
||||
public void Depress(bool depress)
|
||||
{
|
||||
// action can still be toggled if it's allowed to stay selected
|
||||
if (!CanUseAction) return;
|
||||
|
||||
if (Action == null || !Action.Enabled) return;
|
||||
|
||||
if (_depressed && !depress)
|
||||
{
|
||||
// fire the action
|
||||
// no left-click interaction with it on cooldown or revoked
|
||||
_actionsComponent.AttemptAction(this);
|
||||
_actionsUI.System.OnSlotPressed(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 = default;
|
||||
_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 == default) return;
|
||||
|
||||
Action = action;
|
||||
Item = default;
|
||||
_depressed = false;
|
||||
ToggledOn = false;
|
||||
ActionEnabled = false;
|
||||
Cooldown = null;
|
||||
HideTooltip();
|
||||
UpdateIcons();
|
||||
DrawModeChanged();
|
||||
_number.SetMessage(SlotNumberLabel());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -415,18 +323,12 @@ namespace Content.Client.Actions.UI
|
||||
/// </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, EntityUid item, bool actionEnabled)
|
||||
public void Assign(ActionType action)
|
||||
{
|
||||
// already assigned
|
||||
if (Action != null && Action == action && Item == item) return;
|
||||
if (Action != null && Action == action) return;
|
||||
|
||||
Action = action;
|
||||
Item = item;
|
||||
_depressed = false;
|
||||
ToggledOn = false;
|
||||
ActionEnabled = false;
|
||||
Cooldown = null;
|
||||
HideTooltip();
|
||||
UpdateIcons();
|
||||
DrawModeChanged();
|
||||
@@ -438,12 +340,9 @@ namespace Content.Client.Actions.UI
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
if (!HasAssignment) return;
|
||||
if (Action == null) return;
|
||||
Action = null;
|
||||
Item = default;
|
||||
ToggledOn = false;
|
||||
_depressed = false;
|
||||
Cooldown = null;
|
||||
HideTooltip();
|
||||
UpdateIcons();
|
||||
DrawModeChanged();
|
||||
@@ -453,12 +352,8 @@ namespace Content.Client.Actions.UI
|
||||
/// <summary>
|
||||
/// Display the action in this slot (if there is one) as enabled
|
||||
/// </summary>
|
||||
public void EnableAction()
|
||||
public void Enable()
|
||||
{
|
||||
if (ActionEnabled || !HasAssignment) return;
|
||||
|
||||
ActionEnabled = true;
|
||||
_depressed = false;
|
||||
DrawModeChanged();
|
||||
_number.SetMessage(SlotNumberLabel());
|
||||
}
|
||||
@@ -467,11 +362,8 @@ namespace Content.Client.Actions.UI
|
||||
/// Display the action in this slot (if there is one) as disabled.
|
||||
/// The slot is still clickable.
|
||||
/// </summary>
|
||||
public void DisableAction()
|
||||
public void Disable()
|
||||
{
|
||||
if (!ActionEnabled || !HasAssignment) return;
|
||||
|
||||
ActionEnabled = false;
|
||||
_depressed = false;
|
||||
DrawModeChanged();
|
||||
_number.SetMessage(SlotNumberLabel());
|
||||
@@ -481,70 +373,56 @@ namespace Content.Client.Actions.UI
|
||||
{
|
||||
if (SlotNumber > 10) return FormattedMessage.FromMarkup("");
|
||||
var number = Loc.GetString(SlotNumber == 10 ? "0" : SlotNumber.ToString());
|
||||
var color = (ActionEnabled || !HasAssignment) ? EnabledColor : DisabledColor;
|
||||
var color = (Action == null || Action.Enabled) ? EnabledColor : DisabledColor;
|
||||
return FormattedMessage.FromMarkup("[color=" + color + "]" + number + "[/color]");
|
||||
}
|
||||
|
||||
private void UpdateIcons()
|
||||
public void UpdateIcons()
|
||||
{
|
||||
if (!HasAssignment)
|
||||
UpdateItemIcon();
|
||||
|
||||
if (Action == null)
|
||||
{
|
||||
SetActionIcon(null);
|
||||
SetItemIcon(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (HasToggleSprite && ToggledOn && Action != null)
|
||||
{
|
||||
if ((_actionsUI.SelectingTargetFor?.Action == Action || Action.Toggled) && Action.IconOn != null)
|
||||
SetActionIcon(Action.IconOn.Frame0());
|
||||
}
|
||||
else if (Action != null)
|
||||
{
|
||||
SetActionIcon(Action.Icon.Frame0());
|
||||
}
|
||||
|
||||
if (Item != default)
|
||||
{
|
||||
SetItemIcon(IoCManager.Resolve<IEntityManager>().TryGetComponent<ISpriteComponent?>(Item, out var spriteComponent) ? spriteComponent : null);
|
||||
}
|
||||
else
|
||||
{
|
||||
SetItemIcon(null);
|
||||
}
|
||||
SetActionIcon(Action.Icon?.Frame0());
|
||||
}
|
||||
|
||||
private void SetActionIcon(Texture? texture)
|
||||
{
|
||||
if (texture == null || !HasAssignment)
|
||||
if (texture == null || Action == null)
|
||||
{
|
||||
_bigActionIcon.Texture = null;
|
||||
_bigActionIcon.Visible = false;
|
||||
_smallActionIcon.Texture = null;
|
||||
_smallActionIcon.Visible = false;
|
||||
}
|
||||
else if (Action.Provider != null && Action.ItemIconStyle == ItemActionIconStyle.BigItem)
|
||||
{
|
||||
_smallActionIcon.Texture = texture;
|
||||
_smallActionIcon.Modulate = Action.IconColor;
|
||||
_smallActionIcon.Visible = true;
|
||||
_bigActionIcon.Texture = null;
|
||||
_bigActionIcon.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;
|
||||
}
|
||||
|
||||
_bigActionIcon.Texture = texture;
|
||||
_bigActionIcon.Modulate = Action.IconColor;
|
||||
_bigActionIcon.Visible = true;
|
||||
_smallActionIcon.Texture = null;
|
||||
_smallActionIcon.Visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetItemIcon(ISpriteComponent? sprite)
|
||||
private void UpdateItemIcon()
|
||||
{
|
||||
if (sprite == null || !HasAssignment)
|
||||
if (Action?.Provider == null || !IoCManager.Resolve<IEntityManager>().TryGetComponent(Action.Provider.Value, out SpriteComponent sprite))
|
||||
{
|
||||
_bigItemSpriteView.Visible = false;
|
||||
_bigItemSpriteView.Sprite = null;
|
||||
@@ -553,70 +431,48 @@ namespace Content.Client.Actions.UI
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Action is ItemActionPrototype actionPrototype)
|
||||
switch (Action.ItemIconStyle)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
case ItemActionIconStyle.BigItem:
|
||||
_bigItemSpriteView.Visible = true;
|
||||
_bigItemSpriteView.Sprite = sprite;
|
||||
_smallItemSpriteView.Visible = false;
|
||||
_smallItemSpriteView.Sprite = null;
|
||||
break;
|
||||
case ItemActionIconStyle.BigAction:
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
_bigItemSpriteView.Visible = false;
|
||||
_bigItemSpriteView.Sprite = null;
|
||||
_smallItemSpriteView.Visible = false;
|
||||
_smallItemSpriteView.Sprite = null;
|
||||
}
|
||||
_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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void DrawModeChanged()
|
||||
public 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)
|
||||
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)
|
||||
@@ -625,47 +481,50 @@ namespace Content.Client.Actions.UI
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// if it's toggled on, always show the toggled on style (currently same as depressed style)
|
||||
if (ToggledOn)
|
||||
if (Action.Toggled || _actionsUI.SelectingTargetFor == this)
|
||||
{
|
||||
// when there's a toggle sprite, we're showing that sprite instead of highlighting this slot
|
||||
SetOnlyStylePseudoClass(HasToggleSprite ? ContainerButton.StylePseudoClassNormal :
|
||||
SetOnlyStylePseudoClass(Action.IconOn != null ? ContainerButton.StylePseudoClassNormal :
|
||||
ContainerButton.StylePseudoClassPressed);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (!ActionEnabled)
|
||||
if (!Action.Enabled)
|
||||
{
|
||||
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassDisabled);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassNormal);
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
if (!Cooldown.HasValue)
|
||||
if (Action == null || Action.Cooldown == null || !Action.Enabled)
|
||||
{
|
||||
_cooldownGraphic.Visible = false;
|
||||
_cooldownGraphic.Progress = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
var duration = Cooldown.Value.End - Cooldown.Value.Start;
|
||||
var cooldown = Action.Cooldown.Value;
|
||||
var duration = cooldown.End - cooldown.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);
|
||||
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);
|
||||
_cooldownGraphic.Visible = ratio > -1f;
|
||||
if (ratio > -1f)
|
||||
_cooldownGraphic.Visible = true;
|
||||
else
|
||||
{
|
||||
_cooldownGraphic.Visible = false;
|
||||
Action.Cooldown = null;
|
||||
DrawModeChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,18 @@
|
||||
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 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.GameObjects;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Input.Binding;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using static Robust.Client.UserInterface.Controls.BoxContainer;
|
||||
|
||||
namespace Content.Client.Actions.UI
|
||||
@@ -27,13 +22,16 @@ namespace Content.Client.Actions.UI
|
||||
/// </summary>
|
||||
public sealed class ActionsUI : Container
|
||||
{
|
||||
private const float DragDeadZone = 10f;
|
||||
private const float CustomTooltipDelay = 0.4f;
|
||||
private readonly ClientActionsComponent _actionsComponent;
|
||||
private readonly ActionManager _actionManager;
|
||||
private readonly IEntityManager _entityManager;
|
||||
private readonly IGameTiming _gameTiming;
|
||||
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;
|
||||
@@ -75,15 +73,13 @@ namespace Content.Client.Actions.UI
|
||||
/// </summary>
|
||||
public IEnumerable<ActionSlot> Slots => _slots;
|
||||
|
||||
public ActionsUI(ClientActionsComponent actionsComponent)
|
||||
public ActionsUI(ActionsSystem system, ActionsComponent component)
|
||||
{
|
||||
SetValue(LayoutContainer.DebugProperty, true);
|
||||
_actionsComponent = actionsComponent;
|
||||
_actionManager = IoCManager.Resolve<ActionManager>();
|
||||
_entityManager = IoCManager.Resolve<IEntityManager>();
|
||||
_gameTiming = IoCManager.Resolve<IGameTiming>();
|
||||
System = system;
|
||||
Component = component;
|
||||
_gameHud = IoCManager.Resolve<IGameHud>();
|
||||
_menu = new ActionMenu(_actionsComponent, this);
|
||||
_menu = new ActionMenu(this);
|
||||
|
||||
LayoutContainer.SetGrowHorizontal(this, LayoutContainer.GrowDirection.End);
|
||||
LayoutContainer.SetGrowVertical(this, LayoutContainer.GrowDirection.Constrain);
|
||||
@@ -196,7 +192,7 @@ namespace Content.Client.Actions.UI
|
||||
_loadoutContainer.AddChild(nextHotbarIcon);
|
||||
_loadoutContainer.AddChild(new Control { HorizontalExpand = true, SizeFlagsStretchRatio = 1 });
|
||||
|
||||
_slots = new ActionSlot[ClientActionsComponent.Slots];
|
||||
_slots = new ActionSlot[ActionsSystem.Slots];
|
||||
|
||||
_dragShadow = new TextureRect
|
||||
{
|
||||
@@ -207,14 +203,14 @@ namespace Content.Client.Actions.UI
|
||||
};
|
||||
UserInterfaceManager.PopupRoot.AddChild(_dragShadow);
|
||||
|
||||
for (byte i = 0; i < ClientActionsComponent.Slots; i++)
|
||||
for (byte i = 0; i < ActionsSystem.Slots; i++)
|
||||
{
|
||||
var slot = new ActionSlot(this, _menu, actionsComponent, i);
|
||||
var slot = new ActionSlot(this, _menu, i);
|
||||
_slotContainer.AddChild(slot);
|
||||
_slots[i] = slot;
|
||||
}
|
||||
|
||||
DragDropHelper = new DragDropHelper<ActionSlot>(OnBeginActionDrag, OnContinueActionDrag, OnEndActionDrag);
|
||||
DragDropHelper = new DragDropHelper<ActionSlot>(OnBeginActionDrag, OnContinueActionDrag, OnEndActionDrag, DragDeadZone);
|
||||
|
||||
MinSize = (10, 400);
|
||||
}
|
||||
@@ -285,157 +281,61 @@ namespace Content.Client.Actions.UI
|
||||
|
||||
foreach (var actionSlot in Slots)
|
||||
{
|
||||
var assignedActionType = _actionsComponent.Assignments[SelectedHotbar, actionSlot.SlotIndex];
|
||||
if (!assignedActionType.HasValue)
|
||||
var action = System.Assignments[SelectedHotbar, actionSlot.SlotIndex];
|
||||
|
||||
if (action == null)
|
||||
{
|
||||
if (SelectingTargetFor == actionSlot)
|
||||
StopTargeting(true);
|
||||
actionSlot.Clear();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (assignedActionType.Value.TryGetAction(out var actionType))
|
||||
if (Component.Actions.TryGetValue(action, out var actualAction))
|
||||
{
|
||||
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();
|
||||
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 actionType, ActionSlot actionSlot, ActionAssignment? assignedActionType)
|
||||
private void UpdateActionSlot(ActionType action, ActionSlot actionSlot)
|
||||
{
|
||||
if (_actionManager.TryGet(actionType, out var action))
|
||||
{
|
||||
actionSlot.Assign(action, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.ErrorS("action", "unrecognized actionType {0}", assignedActionType);
|
||||
actionSlot.Clear();
|
||||
return;
|
||||
}
|
||||
actionSlot.Assign(action);
|
||||
|
||||
if (!_actionsComponent.TryGetActionState(actionType, out var actionState) || !actionState.Enabled)
|
||||
if (!action.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;
|
||||
actionSlot.Disable();
|
||||
}
|
||||
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();
|
||||
}
|
||||
actionSlot.Enable();
|
||||
}
|
||||
|
||||
// check if we need to toggle it
|
||||
if (action.BehaviorType == BehaviorType.Toggle)
|
||||
{
|
||||
actionSlot.ToggledOn = actionState.ToggledOn;
|
||||
}
|
||||
actionSlot.UpdateIcons();
|
||||
actionSlot.DrawModeChanged();
|
||||
}
|
||||
|
||||
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.EntityExists(item)) return;
|
||||
if (_actionManager.TryGet(itemActionType, out var action))
|
||||
{
|
||||
actionSlot.Assign(action, item, 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 == item &&
|
||||
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
|
||||
@@ -445,11 +345,11 @@ namespace Content.Client.Actions.UI
|
||||
var rightness = args.RelativePosition.X / _loadoutContainer.Width;
|
||||
if (rightness > 0.5)
|
||||
{
|
||||
ChangeHotbar((byte) ((SelectedHotbar + 1) % ClientActionsComponent.Hotbars));
|
||||
ChangeHotbar((byte) ((SelectedHotbar + 1) % ActionsSystem.Hotbars));
|
||||
}
|
||||
else
|
||||
{
|
||||
var newBar = SelectedHotbar == 0 ? ClientActionsComponent.Hotbars - 1 : SelectedHotbar - 1;
|
||||
var newBar = SelectedHotbar == 0 ? ActionsSystem.Hotbars - 1 : SelectedHotbar - 1;
|
||||
ChangeHotbar((byte) newBar);
|
||||
}
|
||||
}
|
||||
@@ -483,29 +383,35 @@ namespace Content.Client.Actions.UI
|
||||
/// </summary>
|
||||
private void StartTargeting(ActionSlot actionSlot)
|
||||
{
|
||||
if (actionSlot.Action == null)
|
||||
return;
|
||||
|
||||
// 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;
|
||||
}
|
||||
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()
|
||||
public void StopTargeting(bool updating = false)
|
||||
{
|
||||
if (SelectingTargetFor == null) return;
|
||||
if (SelectingTargetFor.ToggledOn)
|
||||
{
|
||||
SelectingTargetFor.ToggledOn = 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)
|
||||
@@ -513,7 +419,6 @@ namespace Content.Client.Actions.UI
|
||||
ToggleActionsMenu();
|
||||
}
|
||||
|
||||
|
||||
private void OnToggleActionsMenuTopButton(bool open)
|
||||
{
|
||||
if (open == _menu.IsOpen) return;
|
||||
@@ -543,7 +448,7 @@ namespace Content.Client.Actions.UI
|
||||
// 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();
|
||||
_dragShadow.Texture = DragDropHelper.Dragged.Action.Icon?.Frame0();
|
||||
LayoutContainer.SetPosition(_dragShadow, UserInterfaceManager.MousePositionScaled.Position - (32, 32));
|
||||
DragDropHelper.Dragged.CancelPress();
|
||||
return true;
|
||||
@@ -574,6 +479,7 @@ namespace Content.Client.Actions.UI
|
||||
{
|
||||
var actionSlot = _slots[slot];
|
||||
actionSlot.Depress(args.State == BoundKeyState.Down);
|
||||
actionSlot.DrawModeChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user