diff --git a/Content.Client/GameObjects/Components/Mobs/ClientActionsComponent.cs b/Content.Client/GameObjects/Components/Mobs/ClientActionsComponent.cs
index 2040dcbbf1..0ccaf31299 100644
--- a/Content.Client/GameObjects/Components/Mobs/ClientActionsComponent.cs
+++ b/Content.Client/GameObjects/Components/Mobs/ClientActionsComponent.cs
@@ -184,8 +184,10 @@ namespace Content.Client.GameObjects.Components.Mobs
// only do something for actual target-based actions
if (_ui?.SelectingTargetFor?.Action == null ||
- (_ui.SelectingTargetFor.Action.BehaviorType != BehaviorType.TargetEntity &&
- _ui.SelectingTargetFor.Action.BehaviorType != BehaviorType.TargetPoint)) return false;
+ (!_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)
@@ -217,6 +219,13 @@ namespace Content.Client.GameObjects.Components.Mobs
}
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;
diff --git a/Content.Client/UserInterface/ActionMenu.cs b/Content.Client/UserInterface/ActionMenu.cs
index 06b6312267..31d1a8ad55 100644
--- a/Content.Client/UserInterface/ActionMenu.cs
+++ b/Content.Client/UserInterface/ActionMenu.cs
@@ -404,8 +404,7 @@ namespace Content.Client.UserInterface
ItemTag => action is ItemActionPrototype,
NotItemTag => action is ActionPrototype,
InstantActionTag => action.BehaviorType == BehaviorType.Instant,
- TargetActionTag => action.BehaviorType == BehaviorType.TargetEntity ||
- action.BehaviorType == BehaviorType.TargetPoint,
+ TargetActionTag => action.IsTargetAction,
ToggleActionTag => action.BehaviorType == BehaviorType.Toggle,
_ => action.Filters.Contains(tag)
};
diff --git a/Content.Client/UserInterface/ActionsUI.cs b/Content.Client/UserInterface/ActionsUI.cs
index 7fe3703df8..c0c6085279 100644
--- a/Content.Client/UserInterface/ActionsUI.cs
+++ b/Content.Client/UserInterface/ActionsUI.cs
@@ -337,9 +337,9 @@ namespace Content.Client.UserInterface
actionSlot.EnableAction();
actionSlot.Cooldown = actionState.Cooldown;
- // if we are targeting with an action now on cooldown, stop targeting
+ // 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))
+ actionState.IsOnCooldown(_gameTiming) && action.DeselectOnCooldown)
{
StopTargeting();
}
@@ -410,10 +410,10 @@ namespace Content.Client.UserInterface
// action is currently granted
actionSlot.EnableAction();
- // if we are targeting with an action now on cooldown, stop targeting
+ // if we are targeting with an action now on cooldown, stop targeting if we should
if (SelectingTargetFor?.Action != null && SelectingTargetFor.Action == action &&
SelectingTargetFor.Item == itemEntity &&
- actionState.IsOnCooldown(_gameTiming))
+ actionState.IsOnCooldown(_gameTiming) && action.DeselectOnCooldown)
{
StopTargeting();
}
diff --git a/Content.Client/UserInterface/Controls/ActionSlot.cs b/Content.Client/UserInterface/Controls/ActionSlot.cs
index 68ea495db6..e98f898d7c 100644
--- a/Content.Client/UserInterface/Controls/ActionSlot.cs
+++ b/Content.Client/UserInterface/Controls/ActionSlot.cs
@@ -53,8 +53,10 @@ namespace Content.Client.UserInterface.Controls
///
/// 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
///
- public bool CanUseAction => HasAssignment && ActionEnabled && !IsOnCooldown;
+ public bool CanUseAction => Action != null && ActionEnabled &&
+ (!IsOnCooldown || (Action.IsTargetAction && !Action.DeselectOnCooldown));
///
/// Item the action is provided by, only valid if Action is an ItemActionPrototype. May be null
@@ -340,8 +342,10 @@ namespace Content.Client.UserInterface.Controls
///
public void Depress(bool depress)
{
+ // action can still be toggled if it's allowed to stay selected
if (!CanUseAction) return;
+
if (_depressed && !depress)
{
// fire the action
diff --git a/Content.Shared/Actions/BaseActionPrototype.cs b/Content.Shared/Actions/BaseActionPrototype.cs
index 4068c9da79..669ee18254 100644
--- a/Content.Shared/Actions/BaseActionPrototype.cs
+++ b/Content.Shared/Actions/BaseActionPrototype.cs
@@ -30,8 +30,6 @@ namespace Content.Shared.Actions
[ViewVariables]
public SpriteSpecifier IconOn { get; private set; }
-
-
///
/// Name to show in UI. Accepts formatting.
///
@@ -60,6 +58,18 @@ namespace Content.Shared.Actions
///
public bool Repeat { get; private set; }
+ ///
+ /// For TargetEntity/TargetPoint actions, should the action be de-selected if currently selected (choosing a target)
+ /// when it goes on cooldown. Defaults to false.
+ ///
+ public bool DeselectOnCooldown { get; private set; }
+
+ ///
+ /// For TargetEntity actions, should the action be de-selected if the user doesn't click an entity when
+ /// selecting a target. Defaults to false.
+ ///
+ public bool DeselectWhenEntityNotClicked { get; private set; }
+
///
/// Filters that can be used to filter this item in action menu.
///
@@ -70,6 +80,12 @@ namespace Content.Shared.Actions
///
public IEnumerable Keywords { get; private set; }
+ ///
+ /// True if this is an action that requires selecting a target
+ ///
+ public bool IsTargetAction =>
+ BehaviorType == BehaviorType.TargetEntity || BehaviorType == BehaviorType.TargetPoint;
+
public virtual void LoadFrom(YamlMappingNode mapping)
{
var serializer = YamlObjectSerializer.NewReader(mapping);
@@ -106,6 +122,9 @@ namespace Content.Shared.Actions
Name, BehaviorType);
}
+ serializer.DataField(this, x => x.DeselectOnCooldown, "deselectOnCooldown", false);
+ serializer.DataField(this, x => x.DeselectWhenEntityNotClicked, "deselectWhenEntityNotClicked", false);
+
serializer.DataReadFunction("filters", new List(),
rawTags =>
{