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 9453502832..b606395f58 100644
Binary files a/Resources/Textures/Interface/Nano/left_arrow.svg.png and b/Resources/Textures/Interface/Nano/left_arrow.svg.png differ
diff --git a/Resources/Textures/Interface/Nano/right_arrow.svg.png b/Resources/Textures/Interface/Nano/right_arrow.svg.png
index 9ed4cb62fa..4a75a88e76 100644
Binary files a/Resources/Textures/Interface/Nano/right_arrow.svg.png and b/Resources/Textures/Interface/Nano/right_arrow.svg.png differ
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