diff --git a/Content.Client/Actions/ActionsSystem.cs b/Content.Client/Actions/ActionsSystem.cs index 3611287269..d68e9326d4 100644 --- a/Content.Client/Actions/ActionsSystem.cs +++ b/Content.Client/Actions/ActionsSystem.cs @@ -180,24 +180,18 @@ namespace Content.Client.Actions private void OnPlayerAttached(EntityUid uid, ActionsComponent component, PlayerAttachedEvent args) { - if (uid != _playerManager.LocalPlayer?.ControlledEntity) - return; - LinkAllActions(component); } private void OnPlayerDetached(EntityUid uid, ActionsComponent component, PlayerDetachedEvent? args = null) { - if (uid != _playerManager.LocalPlayer?.ControlledEntity) - return; - UnlinkAllActions(); } public void UnlinkAllActions() { - UnlinkActions?.Invoke(); PlayerActions = null; + UnlinkActions?.Invoke(); } public void LinkAllActions(ActionsComponent? actions = null) diff --git a/Content.Client/Outline/TargetOutlineSystem.cs b/Content.Client/Outline/TargetOutlineSystem.cs index 32275fa260..b7a338798f 100644 --- a/Content.Client/Outline/TargetOutlineSystem.cs +++ b/Content.Client/Outline/TargetOutlineSystem.cs @@ -71,8 +71,8 @@ public sealed class TargetOutlineSystem : EntitySystem { base.Initialize(); - _shaderTargetValid = _prototypeManager.Index(ShaderTargetValid).Instance(); - _shaderTargetInvalid = _prototypeManager.Index(ShaderTargetInvalid).Instance(); + _shaderTargetValid = _prototypeManager.Index(ShaderTargetValid).InstanceUnique(); + _shaderTargetInvalid = _prototypeManager.Index(ShaderTargetInvalid).InstanceUnique(); } public void Disable() diff --git a/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs b/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs index 1685086be8..2a3bda1e6a 100644 --- a/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs +++ b/Content.Client/UserInterface/Systems/Actions/ActionUIController.cs @@ -1,6 +1,7 @@ using System.Linq; using System.Runtime.InteropServices; using Content.Client.Actions; +using Content.Client.Construction; using Content.Client.DragDrop; using Content.Client.Gameplay; using Content.Client.Hands; @@ -14,6 +15,7 @@ using Content.Shared.Actions.ActionTypes; using Content.Shared.Input; using Robust.Client.GameObjects; using Robust.Client.Graphics; +using Robust.Client.Player; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controllers; using Robust.Client.UserInterface.Controls; @@ -38,6 +40,8 @@ public sealed class ActionUIController : UIController, IOnStateChanged /// Action slot we are currently selecting a target for. /// - public ActionButton? SelectingTargetFor { get; private set; } + public TargetedAction? SelectingTargetFor { get; private set; } = null; public ActionUIController() { @@ -139,9 +143,130 @@ public sealed class ActionUIController : UIController, IOnStateChanged ToggleWindow())) + .BindBefore(EngineKeyFunctions.Use, new PointerInputCmdHandler(TargetingOnUse, outsidePrediction: true), + typeof(ConstructionSystem), typeof(DragDropSystem)) + .BindBefore(EngineKeyFunctions.UIRightClick, new PointerInputCmdHandler(TargetingCancel, outsidePrediction: true)) .Register(); } + private bool TargetingCancel(in PointerInputCmdArgs args) + { + if (!_timing.IsFirstTimePredicted) + return false; + + // only do something for actual target-based actions + if (SelectingTargetFor == null) + return false; + + StopTargeting(); + return true; + } + + /// + /// If the user clicked somewhere, and they are currently targeting an action, try and perform it. + /// + private bool TargetingOnUse(in PointerInputCmdArgs args) + { + if (!_timing.IsFirstTimePredicted || _actionsSystem == null || SelectingTargetFor is not { } action) + return false; + + if (_playerManager.LocalPlayer?.ControlledEntity is not EntityUid user) + return false; + + if (!_entities.TryGetComponent(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 > _timing.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 TryTargetEntity(args, entTarget, user, comp) || !action.InteractOnMiss; + + default: + Logger.Error($"Unknown targeting action: {action.GetType()}"); + return false; + } + } + + private bool TryTargetWorld(in PointerInputCmdArgs args, WorldTargetAction action, EntityUid user, ActionsComponent actionComp) + { + if (_actionsSystem == null) + return false; + + var coords = args.Coordinates.ToMap(_entities); + + if (!_actionsSystem.ValidateWorldTarget(user, coords, action)) + { + // Invalid target. + if (action.DeselectOnMiss) + StopTargeting(); + + return false; + } + + if (action.ClientExclusive) + { + if (action.Event != null) + { + action.Event.Target = coords; + action.Event.Performer = user; + } + + _actionsSystem.PerformAction(actionComp, action, action.Event, _timing.CurTime); + } + else + _entities.RaisePredictiveEvent(new RequestPerformActionEvent(action, coords)); + + if (!action.Repeat) + StopTargeting(); + + return true; + } + + private bool TryTargetEntity(in PointerInputCmdArgs args, EntityTargetAction action, EntityUid user, ActionsComponent actionComp) + { + if (_actionsSystem == null) + return false; + + if (!_actionsSystem.ValidateEntityTarget(user, args.EntityUid, action)) + { + if (action.DeselectOnMiss) + StopTargeting(); + + return false; + } + + if (action.ClientExclusive) + { + if (action.Event != null) + { + action.Event.Target = args.EntityUid; + action.Event.Performer = user; + } + + _actionsSystem.PerformAction(actionComp, action, action.Event, _timing.CurTime); + } + else + _entities.RaisePredictiveEvent(new RequestPerformActionEvent(action, args.EntityUid)); + + if (!action.Repeat) + StopTargeting(); + + return true; + } + public void UnloadButton() { if (ActionButton == null) @@ -204,7 +329,10 @@ public sealed class ActionUIController : UIController, IOnStateChanged actions) @@ -405,7 +534,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged? actions = _actionsSystem?.PlayerActions?.Actions; + IEnumerable? actions = (component ?? _actionsSystem?.PlayerActions)?.Actions; actions ??= Array.Empty(); if (filters.Count == 0 && string.IsNullOrWhiteSpace(search)) @@ -553,19 +682,20 @@ public sealed class ActionUIController : UIController, IOnStateChanged /// - public void ToggleTargeting(ActionButton slot) + public void ToggleTargeting(TargetedAction action) { - if (SelectingTargetFor == slot) + if (SelectingTargetFor == action) { StopTargeting(); return; } - StartTargeting(slot); + StartTargeting(action); } /// /// Puts us in targeting mode, where we need to pick either a target point or entity /// - private void StartTargeting(ActionButton actionSlot) + private void StartTargeting(TargetedAction action) { - if (actionSlot.Action == null) - return; - // If we were targeting something else we should stop StopTargeting(); - SelectingTargetFor = actionSlot; - - if (actionSlot.Action is not TargetedAction action) - return; + SelectingTargetFor = action; // override "held-item" overlay if (action.TargetingIndicator && _overlays.TryGetOverlay(out var handOverlay)) diff --git a/Content.Client/UserInterface/Systems/Actions/Controls/ActionButton.cs b/Content.Client/UserInterface/Systems/Actions/Controls/ActionButton.cs index c069d8514f..9f05ed4d98 100644 --- a/Content.Client/UserInterface/Systems/Actions/Controls/ActionButton.cs +++ b/Content.Client/UserInterface/Systems/Actions/Controls/ActionButton.cs @@ -1,4 +1,4 @@ -using Content.Client.Actions.UI; +using Content.Client.Actions.UI; using Content.Client.Cooldown; using Content.Client.Stylesheets; using Content.Shared.Actions; @@ -274,7 +274,7 @@ public sealed class ActionButton : Control return; } - if ((Controller.SelectingTargetFor?.Action == Action || Action.Toggled) && Action.IconOn != null) + if ((Controller.SelectingTargetFor == Action || Action.Toggled) && Action.IconOn != null) SetActionIcon(Action.IconOn.Frame0()); else SetActionIcon(Action.Icon?.Frame0()); @@ -391,7 +391,7 @@ public sealed class ActionButton : Control } // if it's toggled on, always show the toggled on style (currently same as depressed style) - if (Action.Toggled || Controller.SelectingTargetFor == this) + if (Action.Toggled || Controller.SelectingTargetFor == Action) { // when there's a toggle sprite, we're showing that sprite instead of highlighting this slot SetOnlyStylePseudoClass(Action.IconOn != null diff --git a/Content.Client/UserInterface/Systems/Actions/Controls/ActionButtonContainer.cs b/Content.Client/UserInterface/Systems/Actions/Controls/ActionButtonContainer.cs index 69b469cfe6..5dfa53f5ab 100644 --- a/Content.Client/UserInterface/Systems/Actions/Controls/ActionButtonContainer.cs +++ b/Content.Client/UserInterface/Systems/Actions/Controls/ActionButtonContainer.cs @@ -1,4 +1,4 @@ -using Content.Shared.Actions.ActionTypes; +using Content.Shared.Actions.ActionTypes; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; @@ -7,8 +7,6 @@ namespace Content.Client.UserInterface.Systems.Actions.Controls; [Virtual] public class ActionButtonContainer : GridContainer { - [Dependency] private readonly IEntityManager _entityManager = default!; - public event Action? ActionPressed; public event Action? ActionUnpressed; public event Action? ActionFocusExited; diff --git a/Content.Client/Weapons/Melee/MeleeWeaponSystem.cs b/Content.Client/Weapons/Melee/MeleeWeaponSystem.cs index 8be14b9df7..98279528ac 100644 --- a/Content.Client/Weapons/Melee/MeleeWeaponSystem.cs +++ b/Content.Client/Weapons/Melee/MeleeWeaponSystem.cs @@ -81,6 +81,8 @@ public sealed partial class MeleeWeaponSystem : SharedMeleeWeaponSystem return; } + // TODO using targeted actions while combat mode is enabled should NOT trigger attacks. + var useDown = _inputSystem.CmdStates.GetState(EngineKeyFunctions.Use); var altDown = _inputSystem.CmdStates.GetState(EngineKeyFunctions.UseSecondary); var currentTime = Timing.CurTime; diff --git a/Content.Shared/Actions/SharedActionsSystem.cs b/Content.Shared/Actions/SharedActionsSystem.cs index 15ea245847..67110fa338 100644 --- a/Content.Shared/Actions/SharedActionsSystem.cs +++ b/Content.Shared/Actions/SharedActionsSystem.cs @@ -268,7 +268,7 @@ public abstract class SharedActionsSystem : EntitySystem return _interactionSystem.InRangeUnobstructed(user, coords, range: action.Range); } - protected void PerformAction(ActionsComponent component, ActionType action, BaseActionEvent? actionEvent, TimeSpan curTime) + public void PerformAction(ActionsComponent component, ActionType action, BaseActionEvent? actionEvent, TimeSpan curTime) { var handled = false; diff --git a/Resources/Locale/en-US/actions/actions/dragon.ftl b/Resources/Locale/en-US/actions/actions/dragon.ftl index 32134279c9..6575a7b379 100644 --- a/Resources/Locale/en-US/actions/actions/dragon.ftl +++ b/Resources/Locale/en-US/actions/actions/dragon.ftl @@ -8,8 +8,8 @@ dragon-spawn-action-popup-message-fail-no-eggs = You don't have the stamina to d action-name-devour = [color=red]Devour[/color] action-description-devour = Attempt to break a structure with your jaws or swallow a creature. -action-name-carp-summon = Summon carp -action-description-carp-summon = Summon a carp to aid you at seizing the station! +action-name-carp-rift = Summon Carp Rift +action-description-carp-rift = Summons a carp rift that will periodically spawns carps. # Rifts carp-rift-warning = A rift is causing an unnaturally large energy flux at {$location}. Stop it at all costs!