From 9a3dee2042d5244c01880bae87761cbc825a9631 Mon Sep 17 00:00:00 2001 From: chairbender Date: Tue, 22 Dec 2020 06:41:56 -0800 Subject: [PATCH] Hotbar Improvements + Item Action Integration Test (#2749) * my IDE keeps wanting to change this so.... * Add item actions integration test, fix bug where empty item action dict was left in SharedActionsComponent state * bigger hotbar arrows * nice wide hotbar pagination hitboxes * add ability to switch hotbar loadout via keybinds * always highlight on drag over of actions hotbar * dont rely on content entity for integration test --- .../Components/Mobs/ClientActionsComponent.cs | 7 +- .../EntitySystems/ActionsSystem.cs | 32 +++ Content.Client/Input/ContentContexts.cs | 9 + Content.Client/UserInterface/ActionMenu.cs | 6 + Content.Client/UserInterface/ActionsUI.cs | 72 ++++--- .../UserInterface/Controls/ActionSlot.cs | 27 ++- .../UserInterface/OptionsMenu.KeyRebind.cs | 9 + .../UserInterface/TutorialWindow.cs | 20 +- .../Components/Mobs/ActionsComponentTests.cs | 203 +++++++++++++++++- .../Components/Mobs/SharedActionsComponent.cs | 4 + Content.Shared/Input/ContentKeyFunctions.cs | 9 + .../Interface/Nano/left_arrow.svg.png | Bin 310 -> 395 bytes .../Interface/Nano/right_arrow.svg.png | Bin 309 -> 449 bytes Resources/keybinds.yml | 36 ++++ 14 files changed, 386 insertions(+), 48 deletions(-) diff --git a/Content.Client/GameObjects/Components/Mobs/ClientActionsComponent.cs b/Content.Client/GameObjects/Components/Mobs/ClientActionsComponent.cs index bbcf23ff36..2040dcbbf1 100644 --- a/Content.Client/GameObjects/Components/Mobs/ClientActionsComponent.cs +++ b/Content.Client/GameObjects/Components/Mobs/ClientActionsComponent.cs @@ -27,7 +27,7 @@ namespace Content.Client.GameObjects.Components.Mobs [ComponentReference(typeof(SharedActionsComponent))] public sealed class ClientActionsComponent : SharedActionsComponent { - public const byte Hotbars = 10; + public const byte Hotbars = 9; public const byte Slots = 10; [Dependency] private readonly IPlayerManager _playerManager = default!; @@ -106,6 +106,11 @@ namespace Content.Client.GameObjects.Components.Mobs _ui?.HandleHotbarKeybind(slot, args); } + public void HandleChangeHotbarKeybind(byte hotbar, in PointerInputCmdHandler.PointerInputCmdArgs args) + { + _ui?.HandleChangeHotbarKeybind(hotbar, args); + } + /// /// Updates the displayed hotbar (and menu) based on current state of actions. /// diff --git a/Content.Client/GameObjects/EntitySystems/ActionsSystem.cs b/Content.Client/GameObjects/EntitySystems/ActionsSystem.cs index b30d6c0cff..4656b350a7 100644 --- a/Content.Client/GameObjects/EntitySystems/ActionsSystem.cs +++ b/Content.Client/GameObjects/EntitySystems/ActionsSystem.cs @@ -42,6 +42,24 @@ namespace Content.Client.GameObjects.EntitySystems HandleHotbarKeybind(8)) .Bind(ContentKeyFunctions.Hotbar0, HandleHotbarKeybind(9)) + .Bind(ContentKeyFunctions.Loadout1, + HandleChangeHotbarKeybind(0)) + .Bind(ContentKeyFunctions.Loadout2, + HandleChangeHotbarKeybind(1)) + .Bind(ContentKeyFunctions.Loadout3, + HandleChangeHotbarKeybind(2)) + .Bind(ContentKeyFunctions.Loadout4, + HandleChangeHotbarKeybind(3)) + .Bind(ContentKeyFunctions.Loadout5, + HandleChangeHotbarKeybind(4)) + .Bind(ContentKeyFunctions.Loadout6, + HandleChangeHotbarKeybind(5)) + .Bind(ContentKeyFunctions.Loadout7, + HandleChangeHotbarKeybind(6)) + .Bind(ContentKeyFunctions.Loadout8, + HandleChangeHotbarKeybind(7)) + .Bind(ContentKeyFunctions.Loadout9, + HandleChangeHotbarKeybind(8)) // when selecting a target, we intercept clicks in the game world, treating them as our target selection. We want to // take priority before any other systems handle the click. .BindBefore(EngineKeyFunctions.Use, new PointerInputCmdHandler(TargetingOnUse), @@ -66,6 +84,20 @@ namespace Content.Client.GameObjects.EntitySystems actionsComponent.HandleHotbarKeybind(slot, args); return true; + }); + } + + private PointerInputCmdHandler HandleChangeHotbarKeybind(byte hotbar) + { + // delegate to the ActionsUI, simulating a click on it + return new((in PointerInputCmdHandler.PointerInputCmdArgs args) => + { + var playerEntity = _playerManager.LocalPlayer.ControlledEntity; + if (playerEntity == null || + !playerEntity.TryGetComponent( out var actionsComponent)) return false; + + actionsComponent.HandleChangeHotbarKeybind(hotbar, args); + return true; }, false); } diff --git a/Content.Client/Input/ContentContexts.cs b/Content.Client/Input/ContentContexts.cs index c10d16857f..b12c0a83d5 100644 --- a/Content.Client/Input/ContentContexts.cs +++ b/Content.Client/Input/ContentContexts.cs @@ -57,6 +57,15 @@ namespace Content.Client.Input human.AddFunction(ContentKeyFunctions.Hotbar7); human.AddFunction(ContentKeyFunctions.Hotbar8); human.AddFunction(ContentKeyFunctions.Hotbar9); + human.AddFunction(ContentKeyFunctions.Loadout1); + human.AddFunction(ContentKeyFunctions.Loadout2); + human.AddFunction(ContentKeyFunctions.Loadout3); + human.AddFunction(ContentKeyFunctions.Loadout4); + human.AddFunction(ContentKeyFunctions.Loadout5); + human.AddFunction(ContentKeyFunctions.Loadout6); + human.AddFunction(ContentKeyFunctions.Loadout7); + human.AddFunction(ContentKeyFunctions.Loadout8); + human.AddFunction(ContentKeyFunctions.Loadout9); var ghost = contexts.New("ghost", "common"); ghost.AddFunction(EngineKeyFunctions.MoveUp); diff --git a/Content.Client/UserInterface/ActionMenu.cs b/Content.Client/UserInterface/ActionMenu.cs index 07973f9470..c51d27ac86 100644 --- a/Content.Client/UserInterface/ActionMenu.cs +++ b/Content.Client/UserInterface/ActionMenu.cs @@ -39,6 +39,11 @@ namespace Content.Client.UserInterface private static readonly Regex Whitespace = new Regex(@"\s+", RegexOptions.Compiled); private static readonly BaseActionPrototype[] EmptyActionList = Array.Empty(); + /// + /// Is an action currently being dragged from this window? + /// + public bool IsDragging => _dragDropHelper.IsDragging; + // parallel list of actions currently selectable in itemList private BaseActionPrototype[] _actionList; @@ -158,6 +163,7 @@ namespace Content.Client.UserInterface protected override void ExitedTree() { base.ExitedTree(); + _dragDropHelper.EndDrag(); _clearButton.OnPressed -= OnClearButtonPressed; _searchBar.OnTextChanged -= OnSearchTextChanged; _filterButton.OnItemSelected -= OnFilterItemSelected; diff --git a/Content.Client/UserInterface/ActionsUI.cs b/Content.Client/UserInterface/ActionsUI.cs index 7f5698a16d..1a7f9d7074 100644 --- a/Content.Client/UserInterface/ActionsUI.cs +++ b/Content.Client/UserInterface/ActionsUI.cs @@ -39,11 +39,10 @@ namespace Content.Client.UserInterface private readonly TextureButton _lockButton; private readonly TextureButton _settingsButton; - private readonly TextureButton _previousHotbarButton; private readonly Label _loadoutNumber; - private readonly TextureButton _nextHotbarButton; private readonly Texture _lockTexture; private readonly Texture _unlockTexture; + private readonly HBoxContainer _loadoutContainer; private readonly TextureRect _dragShadow; @@ -148,38 +147,39 @@ namespace Content.Client.UserInterface }; hotbarContainer.AddChild(_slotContainer); - var loadoutContainer = new HBoxContainer + _loadoutContainer = new HBoxContainer { - SizeFlagsHorizontal = SizeFlags.FillExpand + SizeFlagsHorizontal = SizeFlags.FillExpand, + MouseFilter = MouseFilterMode.Stop }; - hotbarContainer.AddChild(loadoutContainer); + hotbarContainer.AddChild(_loadoutContainer); - loadoutContainer.AddChild(new Control { SizeFlagsHorizontal = SizeFlags.FillExpand, SizeFlagsStretchRatio = 1 }); - _previousHotbarButton = new TextureButton + _loadoutContainer.AddChild(new Control { SizeFlagsHorizontal = SizeFlags.FillExpand, SizeFlagsStretchRatio = 1 }); + var previousHotbarIcon = new TextureRect() { - TextureNormal = resourceCache.GetTexture("/Textures/Interface/Nano/left_arrow.svg.png"), + Texture = resourceCache.GetTexture("/Textures/Interface/Nano/left_arrow.svg.png"), SizeFlagsHorizontal = SizeFlags.ShrinkCenter, SizeFlagsVertical = SizeFlags.ShrinkCenter, SizeFlagsStretchRatio = 1 }; - loadoutContainer.AddChild(_previousHotbarButton); - loadoutContainer.AddChild(new Control { SizeFlagsHorizontal = SizeFlags.FillExpand, SizeFlagsStretchRatio = 2 }); + _loadoutContainer.AddChild(previousHotbarIcon); + _loadoutContainer.AddChild(new Control { SizeFlagsHorizontal = SizeFlags.FillExpand, SizeFlagsStretchRatio = 2 }); _loadoutNumber = new Label { Text = "1", SizeFlagsStretchRatio = 1 }; - loadoutContainer.AddChild(_loadoutNumber); - loadoutContainer.AddChild(new Control { SizeFlagsHorizontal = SizeFlags.FillExpand, SizeFlagsStretchRatio = 2 }); - _nextHotbarButton = new TextureButton + _loadoutContainer.AddChild(_loadoutNumber); + _loadoutContainer.AddChild(new Control { SizeFlagsHorizontal = SizeFlags.FillExpand, SizeFlagsStretchRatio = 2 }); + var nextHotbarIcon = new TextureRect { - TextureNormal = resourceCache.GetTexture("/Textures/Interface/Nano/right_arrow.svg.png"), + Texture = resourceCache.GetTexture("/Textures/Interface/Nano/right_arrow.svg.png"), SizeFlagsHorizontal = SizeFlags.ShrinkCenter, SizeFlagsVertical = SizeFlags.ShrinkCenter, SizeFlagsStretchRatio = 1 }; - loadoutContainer.AddChild(_nextHotbarButton); - loadoutContainer.AddChild(new Control { SizeFlagsHorizontal = SizeFlags.FillExpand, SizeFlagsStretchRatio = 1 }); + _loadoutContainer.AddChild(nextHotbarIcon); + _loadoutContainer.AddChild(new Control { SizeFlagsHorizontal = SizeFlags.FillExpand, SizeFlagsStretchRatio = 1 }); _slots = new ActionSlot[ClientActionsComponent.Slots]; @@ -194,7 +194,7 @@ namespace Content.Client.UserInterface for (byte i = 0; i < ClientActionsComponent.Slots; i++) { - var slot = new ActionSlot(this, actionsComponent, i); + var slot = new ActionSlot(this, _menu, actionsComponent, i); _slotContainer.AddChild(slot); _slots[i] = slot; } @@ -206,9 +206,8 @@ namespace Content.Client.UserInterface { base.EnteredTree(); _lockButton.OnPressed += OnLockPressed; - _nextHotbarButton.OnPressed += NextHotbar; - _previousHotbarButton.OnPressed += PreviousHotbar; _settingsButton.OnPressed += OnToggleActionsMenu; + _loadoutContainer.OnKeyBindDown += OnHotbarPaginate; } protected override void ExitedTree() @@ -217,9 +216,8 @@ namespace Content.Client.UserInterface StopTargeting(); _menu.Close(); _lockButton.OnPressed -= OnLockPressed; - _nextHotbarButton.OnPressed -= NextHotbar; - _previousHotbarButton.OnPressed -= PreviousHotbar; _settingsButton.OnPressed -= OnToggleActionsMenu; + _loadoutContainer.OnKeyBindDown -= OnHotbarPaginate; } protected override Vector2 CalculateMinimumSize() @@ -420,17 +418,24 @@ namespace Content.Client.UserInterface } } - private void NextHotbar(BaseButton.ButtonEventArgs args) - { - ChangeHotbar((byte) ((SelectedHotbar + 1) % ClientActionsComponent.Hotbars)); - } - private void PreviousHotbar(BaseButton.ButtonEventArgs args) + private void OnHotbarPaginate(GUIBoundKeyEventArgs args) { - var newBar = SelectedHotbar == 0 ? ClientActionsComponent.Hotbars - 1 : SelectedHotbar - 1; - ChangeHotbar((byte) newBar); - } + // rather than clicking the arrows themselves, the user can click the hbox so it's more + // "forgiving" for misclicks, and we simply check which side they are closer to + if (args.Function != EngineKeyFunctions.UIClick) return; + var rightness = args.RelativePosition.X / _loadoutContainer.Width; + if (rightness > 0.5) + { + ChangeHotbar((byte) ((SelectedHotbar + 1) % ClientActionsComponent.Hotbars)); + } + else + { + var newBar = SelectedHotbar == 0 ? ClientActionsComponent.Hotbars - 1 : SelectedHotbar - 1; + ChangeHotbar((byte) newBar); + } + } private void ChangeHotbar(byte hotbar) { @@ -547,6 +552,15 @@ namespace Content.Client.UserInterface actionSlot.Depress(args.State == BoundKeyState.Down); } + /// + /// Handle hotbar change. + /// + /// hotbar index to switch to + public void HandleChangeHotbarKeybind(byte hotbar, PointerInputCmdHandler.PointerInputCmdArgs args) + { + ChangeHotbar(hotbar); + } + protected override void FrameUpdate(FrameEventArgs args) { base.Update(args); diff --git a/Content.Client/UserInterface/Controls/ActionSlot.cs b/Content.Client/UserInterface/Controls/ActionSlot.cs index 0e1f24a86a..68ea495db6 100644 --- a/Content.Client/UserInterface/Controls/ActionSlot.cs +++ b/Content.Client/UserInterface/Controls/ActionSlot.cs @@ -110,6 +110,7 @@ namespace Content.Client.UserInterface.Controls private readonly SpriteView _bigItemSpriteView; 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. @@ -120,10 +121,11 @@ namespace Content.Client.UserInterface.Controls /// Creates an action slot for the specified number /// /// slot index this corresponds to, 0-9 (0 labeled as 1, 8, labeled "9", 9 labeled as "0". - public ActionSlot(ActionsUI actionsUI, ClientActionsComponent actionsComponent, byte slotIndex) + public ActionSlot(ActionsUI actionsUI, ActionMenu actionMenu, ClientActionsComponent actionsComponent, byte slotIndex) { _actionsComponent = actionsComponent; _actionsUI = actionsUI; + _actionMenu = actionMenu; _gameTiming = IoCManager.Resolve(); SlotIndex = slotIndex; MouseFilter = MouseFilterMode.Stop; @@ -259,7 +261,7 @@ namespace Content.Client.UserInterface.Controls if (args.Function == EngineKeyFunctions.UIRightClick) { - if (!_actionsUI.Locked && !_actionsUI.DragDropHelper.IsDragging) + if (!_actionsUI.Locked && !_actionsUI.DragDropHelper.IsDragging && !_actionMenu.IsDragging) { _actionsComponent.Assignments.ClearSlot(_actionsUI.SelectedHotbar, SlotIndex, true); _actionsUI.StopTargeting(); @@ -582,6 +584,18 @@ namespace Content.Client.UserInterface.Controls private 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) { @@ -597,15 +611,6 @@ namespace Content.Client.UserInterface.Controls return; } - // show a hover only if the action is usable - if (_beingHovered) - { - if (ActionEnabled && !IsOnCooldown) - { - SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassHover); - return; - } - } // if it's toggled on, always show the toggled on style (currently same as depressed style) if (ToggledOn) diff --git a/Content.Client/UserInterface/OptionsMenu.KeyRebind.cs b/Content.Client/UserInterface/OptionsMenu.KeyRebind.cs index c208e42536..f2448f6516 100644 --- a/Content.Client/UserInterface/OptionsMenu.KeyRebind.cs +++ b/Content.Client/UserInterface/OptionsMenu.KeyRebind.cs @@ -180,6 +180,15 @@ namespace Content.Client.UserInterface AddButton(ContentKeyFunctions.Hotbar8, "Hotbar slot 8"); AddButton(ContentKeyFunctions.Hotbar9, "Hotbar slot 9"); AddButton(ContentKeyFunctions.Hotbar0, "Hotbar slot 0"); + AddButton(ContentKeyFunctions.Loadout1, "Hotbar Loadout 1"); + AddButton(ContentKeyFunctions.Loadout2, "Hotbar Loadout 2"); + AddButton(ContentKeyFunctions.Loadout3, "Hotbar Loadout 3"); + AddButton(ContentKeyFunctions.Loadout4, "Hotbar Loadout 4"); + AddButton(ContentKeyFunctions.Loadout5, "Hotbar Loadout 5"); + AddButton(ContentKeyFunctions.Loadout6, "Hotbar Loadout 6"); + AddButton(ContentKeyFunctions.Loadout7, "Hotbar Loadout 7"); + AddButton(ContentKeyFunctions.Loadout8, "Hotbar Loadout 8"); + AddButton(ContentKeyFunctions.Loadout9, "Hotbar Loadout 9"); AddHeader("Map Editor"); AddButton(EngineKeyFunctions.EditorPlaceObject, "Place object"); diff --git a/Content.Client/UserInterface/TutorialWindow.cs b/Content.Client/UserInterface/TutorialWindow.cs index ca1c4c0c53..3bd614e109 100644 --- a/Content.Client/UserInterface/TutorialWindow.cs +++ b/Content.Client/UserInterface/TutorialWindow.cs @@ -106,6 +106,15 @@ Hotbar slot 7: [color=#a4885c]{40}[/color] Hotbar slot 8: [color=#a4885c]{41}[/color] Hotbar slot 9: [color=#a4885c]{42}[/color] Hotbar slot 0: [color=#a4885c]{43}[/color] +Hotbar Loadout 1: [color=#a4885c]{44}[/color] +Hotbar Loadout 2: [color=#a4885c]{45}[/color] +Hotbar Loadout 3: [color=#a4885c]{46}[/color] +Hotbar Loadout 4: [color=#a4885c]{47}[/color] +Hotbar Loadout 5: [color=#a4885c]{48}[/color] +Hotbar Loadout 6: [color=#a4885c]{49}[/color] +Hotbar Loadout 7: [color=#a4885c]{50}[/color] +Hotbar Loadout 8: [color=#a4885c]{51}[/color] +Hotbar Loadout 9: [color=#a4885c]{52}[/color] ", Key(MoveUp), Key(MoveLeft), Key(MoveDown), Key(MoveRight), Key(SwapHands), @@ -147,7 +156,16 @@ Hotbar slot 0: [color=#a4885c]{43}[/color] Key(Hotbar7), Key(Hotbar8), Key(Hotbar9), - Key(Hotbar0))); + Key(Hotbar0), + Key(Loadout1), + Key(Loadout2), + Key(Loadout3), + Key(Loadout4), + Key(Loadout5), + Key(Loadout6), + Key(Loadout7), + Key(Loadout8), + Key(Loadout9))); //Gameplay VBox.AddChild(new Label { FontOverride = headerFont, Text = "\nGameplay" }); diff --git a/Content.IntegrationTests/Tests/GameObjects/Components/Mobs/ActionsComponentTests.cs b/Content.IntegrationTests/Tests/GameObjects/Components/Mobs/ActionsComponentTests.cs index 850391c21d..61dde3b4e8 100644 --- a/Content.IntegrationTests/Tests/GameObjects/Components/Mobs/ActionsComponentTests.cs +++ b/Content.IntegrationTests/Tests/GameObjects/Components/Mobs/ActionsComponentTests.cs @@ -1,19 +1,22 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Content.Client.GameObjects.Components.Mobs; using Content.Client.UserInterface; -using Content.Client.UserInterface.Controls; +using Content.Server.GameObjects.Components.GUI; +using Content.Server.GameObjects.Components.Items.Storage; using Content.Server.GameObjects.Components.Mobs; using Content.Shared.Actions; -using Content.Shared.Alert; using Content.Shared.GameObjects.Components.Mobs; -using Content.Shared.GameObjects.EntitySystems; using NUnit.Framework; using Robust.Client.Interfaces.UserInterface; using Robust.Client.Player; +using Robust.Shared.GameObjects; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Map; +using Content.Shared.Utility; +using Robust.Shared.Interfaces.Timing; +using Robust.Shared.Utility; namespace Content.IntegrationTests.Tests.GameObjects.Components.Mobs { @@ -21,8 +24,39 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components.Mobs [TestOf(typeof(SharedActionsComponent))] [TestOf(typeof(ClientActionsComponent))] [TestOf(typeof(ServerActionsComponent))] + [TestOf(typeof(ItemActionsComponent))] public class ActionsComponentTests : ContentIntegrationTest { + const string PROTOTYPES = @" +- type: entity + name: flashlight + parent: BaseItem + id: TestFlashlight + components: + - type: HandheldLight + - type: ItemActions + actions: + - actionType: ToggleLight + - type: PowerCellSlot + - type: Sprite + sprite: Objects/Tools/flashlight.rsi + layers: + - state: lantern_off + - state: HandheldLightOnOverlay + shader: unshaded + visible: false + - type: Item + sprite: Objects/Tools/flashlight.rsi + HeldPrefix: off + - type: PointLight + enabled: false + radius: 3 + - type: LoopingSound + - type: Appearance + visuals: + - type: FlashLightVisualizer +"; + [Test] public async Task GrantsAndRevokesActionsTest() { @@ -47,6 +81,7 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components.Mobs Assert.That(actionsComponent.TryGetActionState(innateAction, out var innateState)); Assert.That(innateState.Enabled); } + Assert.That(innateActions.Count, Is.GreaterThan(0)); actionsComponent.Grant(ActionType.DebugInstant); Assert.That(actionsComponent.TryGetActionState(ActionType.HumanScream, out var state) && state.Enabled); @@ -182,5 +217,161 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components.Mobs }); } + [Test] + public async Task GrantsAndRevokesItemActions() + { + var serverOptions = new ServerIntegrationOptions { ExtraPrototypes = PROTOTYPES }; + var clientOptions = new ClientIntegrationOptions { ExtraPrototypes = PROTOTYPES }; + var (client, server) = await StartConnectedServerClientPair(serverOptions: serverOptions, clientOptions: clientOptions); + + await server.WaitIdleAsync(); + await client.WaitIdleAsync(); + + var serverPlayerManager = server.ResolveDependency(); + var serverEntManager = server.ResolveDependency(); + var serverGameTiming = server.ResolveDependency(); + + var cooldown = Cooldowns.SecondsFromNow(30, serverGameTiming); + + ServerActionsComponent serverActionsComponent = null; + ClientActionsComponent clientActionsComponent = null; + IEntity serverPlayerEnt = null; + IEntity serverFlashlight = null; + + await server.WaitAssertion(() => + { + serverPlayerEnt = serverPlayerManager.GetAllPlayers().Single().AttachedEntity; + serverActionsComponent = serverPlayerEnt.GetComponent(); + + // spawn and give them an item that has actions + serverFlashlight = serverEntManager.SpawnEntity("TestFlashlight", + new EntityCoordinates(new EntityUid(1), (0, 0))); + Assert.That(serverFlashlight.TryGetComponent(out var itemActions)); + // we expect this only to have a toggle light action initially + var actionConfigs = itemActions.ActionConfigs.ToList(); + Assert.That(actionConfigs.Count == 1); + Assert.That(actionConfigs[0].ActionType == ItemActionType.ToggleLight); + Assert.That(actionConfigs[0].Enabled); + + // grant an extra item action, before pickup, initially disabled + itemActions.GrantOrUpdate(ItemActionType.DebugToggle, false); + serverPlayerEnt.GetComponent().PutInHand(serverFlashlight.GetComponent(), false); + // grant an extra item action, after pickup, with a cooldown + itemActions.GrantOrUpdate(ItemActionType.DebugInstant, cooldown: cooldown); + + Assert.That(serverActionsComponent.TryGetItemActionStates(serverFlashlight.Uid, out var state)); + // they should have been granted all 3 actions + Assert.That(state.Count == 3); + Assert.That(state.TryGetValue(ItemActionType.ToggleLight, out var toggleLightState)); + Assert.That(toggleLightState.Equals(new ActionState(true))); + Assert.That(state.TryGetValue(ItemActionType.DebugInstant, out var debugInstantState)); + Assert.That(debugInstantState.Equals(new ActionState(true, cooldown: cooldown))); + Assert.That(state.TryGetValue(ItemActionType.DebugToggle, out var debugToggleState)); + Assert.That(debugToggleState.Equals(new ActionState(false))); + }); + + await server.WaitRunTicks(5); + await client.WaitRunTicks(5); + + // check that client has the actions, and toggle the light on via the action slot it was auto-assigned to + var clientPlayerMgr = client.ResolveDependency(); + var clientUIMgr = client.ResolveDependency(); + var clientEntMgr = client.ResolveDependency(); + EntityUid clientFlashlight = default; + await client.WaitAssertion(() => + { + var local = clientPlayerMgr.LocalPlayer; + var controlled = local.ControlledEntity; + clientActionsComponent = controlled.GetComponent(); + + var lightEntry = clientActionsComponent.ItemActionStates() + .Where(entry => entry.Value.ContainsKey(ItemActionType.ToggleLight)) + .FirstOrNull(); + clientFlashlight = lightEntry.Value.Key; + Assert.That(lightEntry, Is.Not.Null); + Assert.That(lightEntry.Value.Value.TryGetValue(ItemActionType.ToggleLight, out var lightState)); + Assert.That(lightState.Equals(new ActionState(true))); + Assert.That(lightEntry.Value.Value.TryGetValue(ItemActionType.DebugInstant, out var debugInstantState)); + Assert.That(debugInstantState.Equals(new ActionState(true, cooldown: cooldown))); + Assert.That(lightEntry.Value.Value.TryGetValue(ItemActionType.DebugToggle, out var debugToggleState)); + Assert.That(debugToggleState.Equals(new ActionState(false))); + + var actionsUI = clientUIMgr.StateRoot.Children.FirstOrDefault(c => c is ActionsUI) as ActionsUI; + Assert.That(actionsUI, Is.Not.Null); + + var toggleLightSlot = actionsUI.Slots.FirstOrDefault(slot => slot.Action is ItemActionPrototype + { + ActionType: ItemActionType.ToggleLight + }); + Assert.That(toggleLightSlot, Is.Not.Null); + + clientActionsComponent.AttemptAction(toggleLightSlot); + }); + + await server.WaitRunTicks(5); + await client.WaitRunTicks(5); + + // server should see the action toggled on + await server.WaitAssertion(() => + { + Assert.That(serverActionsComponent.ItemActionStates().TryGetValue(serverFlashlight.Uid, out var lightStates)); + Assert.That(lightStates.TryGetValue(ItemActionType.ToggleLight, out var lightState)); + Assert.That(lightState, Is.EqualTo(new ActionState(true, toggledOn: true))); + }); + + // client should see it toggled on. + await client.WaitAssertion(() => + { + Assert.That(clientActionsComponent.ItemActionStates().TryGetValue(clientFlashlight, out var lightStates)); + Assert.That(lightStates.TryGetValue(ItemActionType.ToggleLight, out var lightState)); + Assert.That(lightState, Is.EqualTo(new ActionState(true, toggledOn: true))); + }); + + await server.WaitAssertion(() => + { + // drop the item, and the item actions should go away + serverPlayerEnt.GetComponent() + .Drop(serverFlashlight, serverPlayerEnt.Transform.Coordinates, false); + Assert.That(serverActionsComponent.ItemActionStates().ContainsKey(serverFlashlight.Uid), Is.False); + }); + + await server.WaitRunTicks(5); + await client.WaitRunTicks(5); + + // client should see they have no item actions for that item either. + await client.WaitAssertion(() => + { + Assert.That(clientActionsComponent.ItemActionStates().ContainsKey(clientFlashlight), Is.False); + }); + + await server.WaitAssertion(() => + { + // pick the item up again, the states should be back to what they were when dropped, + // as the states "stick" with the item + serverPlayerEnt.GetComponent().PutInHand(serverFlashlight.GetComponent(), false); + Assert.That(serverActionsComponent.ItemActionStates().TryGetValue(serverFlashlight.Uid, out var lightStates)); + Assert.That(lightStates.TryGetValue(ItemActionType.ToggleLight, out var lightState)); + Assert.That(lightState.Equals(new ActionState(true, toggledOn: true))); + Assert.That(lightStates.TryGetValue(ItemActionType.DebugInstant, out var debugInstantState)); + Assert.That(debugInstantState.Equals(new ActionState(true, cooldown: cooldown))); + Assert.That(lightStates.TryGetValue(ItemActionType.DebugToggle, out var debugToggleState)); + Assert.That(debugToggleState.Equals(new ActionState(false))); + }); + + await server.WaitRunTicks(5); + await client.WaitRunTicks(5); + + // client should see the actions again, with their states back to what they were + await client.WaitAssertion(() => + { + Assert.That(clientActionsComponent.ItemActionStates().TryGetValue(clientFlashlight, out var lightStates)); + Assert.That(lightStates.TryGetValue(ItemActionType.ToggleLight, out var lightState)); + Assert.That(lightState.Equals(new ActionState(true, toggledOn: true))); + Assert.That(lightStates.TryGetValue(ItemActionType.DebugInstant, out var debugInstantState)); + Assert.That(debugInstantState.Equals(new ActionState(true, cooldown: cooldown))); + Assert.That(lightStates.TryGetValue(ItemActionType.DebugToggle, out var debugToggleState)); + Assert.That(debugToggleState.Equals(new ActionState(false))); + }); + } } } diff --git a/Content.Shared/GameObjects/Components/Mobs/SharedActionsComponent.cs b/Content.Shared/GameObjects/Components/Mobs/SharedActionsComponent.cs index 5a9f77a27f..9fe5df8bf8 100644 --- a/Content.Shared/GameObjects/Components/Mobs/SharedActionsComponent.cs +++ b/Content.Shared/GameObjects/Components/Mobs/SharedActionsComponent.cs @@ -265,6 +265,10 @@ namespace Content.Shared.GameObjects.Components.Mobs return; itemStates.Remove(actionType); + if (itemStates.Count == 0) + { + _itemActions.Remove(item); + } AfterActionChanged(); Dirty(); } diff --git a/Content.Shared/Input/ContentKeyFunctions.cs b/Content.Shared/Input/ContentKeyFunctions.cs index 3d9e61a2e0..95ed6f1491 100644 --- a/Content.Shared/Input/ContentKeyFunctions.cs +++ b/Content.Shared/Input/ContentKeyFunctions.cs @@ -52,5 +52,14 @@ namespace Content.Shared.Input public static readonly BoundKeyFunction Hotbar7 = "Hotbar7"; public static readonly BoundKeyFunction Hotbar8 = "Hotbar8"; public static readonly BoundKeyFunction Hotbar9 = "Hotbar9"; + public static readonly BoundKeyFunction Loadout1 = "Loadout1"; + public static readonly BoundKeyFunction Loadout2 = "Loadout2"; + public static readonly BoundKeyFunction Loadout3 = "Loadout3"; + public static readonly BoundKeyFunction Loadout4 = "Loadout4"; + public static readonly BoundKeyFunction Loadout5 = "Loadout5"; + public static readonly BoundKeyFunction Loadout6 = "Loadout6"; + public static readonly BoundKeyFunction Loadout7 = "Loadout7"; + public static readonly BoundKeyFunction Loadout8 = "Loadout8"; + public static readonly BoundKeyFunction Loadout9 = "Loadout9"; } } diff --git a/Resources/Textures/Interface/Nano/left_arrow.svg.png b/Resources/Textures/Interface/Nano/left_arrow.svg.png index 94535028325521eb9044996d0e879e9e52028a09..b606395f58fe4b8246420bfa289fc1f53ba62fa9 100644 GIT binary patch literal 395 zcmV;60d)R}P)JP00009a7bBm000#! z000#!0ilZzLjV8(8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10T@X{ zK~yM_m6E+m15pr#&)nV3N-PCQVWX&xmE;v{^cloRlnB-llDIcqSH(_=R>Az*iLa11 zu(pa=SP6Dk!n*fZh>}3EF;jns;X5;ee@+mzS17!cNno*fH#?J#jtD6M@YQ&fQqj#F zKmGM=AzJ|V0AS<>tvgvO_8O2ykj8Uf8=qKMa;)mRgP<|(Ohleo+8hLRPrOZIwm$%% zWEubf7XS){x;LLm9Y&TRz(++Vdm8ROjoR)8rnNzf8bKeB{qr#YkW|92G?SpAz?sSS z-ru5if!qMRJ+3x;T6fmQ1%_HGm%DQ+6<0wV{79$tfb_CsRe$PQ6E01u=vGcdE&#lc ptcl3|)UPoy@aJc`t16DKz5&E~av}B!%RK-9002ovPDHLkV1jUCp-2D# literal 310 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2V8<6ZZI=>f4aTa()7Bet#3xhBt!>l*8o|0J>k`KLWy978G?YXc7QHU;qH3SKP13m=DGQiE{6k@zC|pO4bbV>7SA1#=i?D!`ixoWeQ8CG ziV5ed^O9kKKQG^K^jPa?a4_JmSHeS=Ls>m+@8g<&1}3Vn;!p&7i^0>?&t;ucLK6TW CP;pZL diff --git a/Resources/Textures/Interface/Nano/right_arrow.svg.png b/Resources/Textures/Interface/Nano/right_arrow.svg.png index 9ed4cb62fa00d06a4297276d779616df52051f94..4a75a88e761fc9690062eb2543e59c777801e74a 100644 GIT binary patch literal 449 zcmV;y0Y3hTP)JP00009a7bBm000#6 z000#60kN{bl>h($8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10ZvIo zK~yM_jgK))n^6>opL3I_q(gr}9Yhfn95Omc!Tf}7f@0$0U`r(u>izP0iA{8fh;ef8 zOCnCXic9I*p%gL}itgef4pxii9tROb{j|?|-simMU`x@rQmHx2e_{}_UVPgwMH|K9 z*4x(w0AvCslk83%2VvurbI!adbWEpRDNm1q%!1Be``(|{MWZh?0Ekw~)B9I{lS6_Y z;Q4ODXwTZ{U5s!?tu}e_wVZe2WdtNmg5N>7?dIm{-2h_4NIS+4th$+_=S~P*lvPh1 zNP#TIKKBR#RE%CG;}Sv87*NBydVMcw?fmRwWpnf6zb8VWu=A!TeZ8P)5p+LgOuuuv zll%DtLB)p64{)=l$SH>Lip&3ayf6u1I+C$rqXqP->X+(rt_7mcooWC?WEbIA%5)7@ rm-EfI@Lhv)=o5+0D{ksfT<9UU5JYw#j{~@+00000NkvXXu0mjf@59FN literal 309 zcmeAS@N?(olHy`uVBq!ia0vp^oIuRQ!3HGLSWEVS1PVMNiy0WWg+Z8+Vb&Z8pde#$ zkh>GZx^prwfgF}}M_)$E)e-c@N{8OGTjv*C{wf%;CO#uQf<@;5lidbwL!!Jlz zFf)ggm59avP+&|}4GU{9XY^NcKX8oc!}@@Xj;`QbvwS1t4I70Ok7V-HA5pomXi?&+ zwI^P-PT0V)?Nx3(1qJpXAM=D=r9ZlcUmdKI;Vst02+yK A-~a#s diff --git a/Resources/keybinds.yml b/Resources/keybinds.yml index 367b8fc756..1a3b1ff275 100644 --- a/Resources/keybinds.yml +++ b/Resources/keybinds.yml @@ -334,3 +334,39 @@ binds: - function: Hotbar9 type: State key: Num9 +- function: Loadout1 + type: State + key: Num1 + mod1: Shift +- function: Loadout2 + type: State + key: Num2 + mod1: Shift +- function: Loadout3 + type: State + key: Num3 + mod1: Shift +- function: Loadout4 + type: State + key: Num4 + mod1: Shift +- function: Loadout5 + type: State + key: Num5 + mod1: Shift +- function: Loadout6 + type: State + key: Num6 + mod1: Shift +- function: Loadout7 + type: State + key: Num7 + mod1: Shift +- function: Loadout8 + type: State + key: Num8 + mod1: Shift +- function: Loadout9 + type: State + key: Num9 + mod1: Shift