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 => {