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
This commit is contained in:
chairbender
2020-12-22 06:41:56 -08:00
committed by GitHub
parent 7f7f22ef5d
commit 9a3dee2042
14 changed files with 386 additions and 48 deletions

View File

@@ -27,7 +27,7 @@ namespace Content.Client.GameObjects.Components.Mobs
[ComponentReference(typeof(SharedActionsComponent))] [ComponentReference(typeof(SharedActionsComponent))]
public sealed class ClientActionsComponent : SharedActionsComponent public sealed class ClientActionsComponent : SharedActionsComponent
{ {
public const byte Hotbars = 10; public const byte Hotbars = 9;
public const byte Slots = 10; public const byte Slots = 10;
[Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IPlayerManager _playerManager = default!;
@@ -106,6 +106,11 @@ namespace Content.Client.GameObjects.Components.Mobs
_ui?.HandleHotbarKeybind(slot, args); _ui?.HandleHotbarKeybind(slot, args);
} }
public void HandleChangeHotbarKeybind(byte hotbar, in PointerInputCmdHandler.PointerInputCmdArgs args)
{
_ui?.HandleChangeHotbarKeybind(hotbar, args);
}
/// <summary> /// <summary>
/// Updates the displayed hotbar (and menu) based on current state of actions. /// Updates the displayed hotbar (and menu) based on current state of actions.
/// </summary> /// </summary>

View File

@@ -42,6 +42,24 @@ namespace Content.Client.GameObjects.EntitySystems
HandleHotbarKeybind(8)) HandleHotbarKeybind(8))
.Bind(ContentKeyFunctions.Hotbar0, .Bind(ContentKeyFunctions.Hotbar0,
HandleHotbarKeybind(9)) 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 // 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. // take priority before any other systems handle the click.
.BindBefore(EngineKeyFunctions.Use, new PointerInputCmdHandler(TargetingOnUse), .BindBefore(EngineKeyFunctions.Use, new PointerInputCmdHandler(TargetingOnUse),
@@ -66,6 +84,20 @@ namespace Content.Client.GameObjects.EntitySystems
actionsComponent.HandleHotbarKeybind(slot, args); actionsComponent.HandleHotbarKeybind(slot, args);
return true; 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<ClientActionsComponent>( out var actionsComponent)) return false;
actionsComponent.HandleChangeHotbarKeybind(hotbar, args);
return true;
}, },
false); false);
} }

View File

@@ -57,6 +57,15 @@ namespace Content.Client.Input
human.AddFunction(ContentKeyFunctions.Hotbar7); human.AddFunction(ContentKeyFunctions.Hotbar7);
human.AddFunction(ContentKeyFunctions.Hotbar8); human.AddFunction(ContentKeyFunctions.Hotbar8);
human.AddFunction(ContentKeyFunctions.Hotbar9); 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"); var ghost = contexts.New("ghost", "common");
ghost.AddFunction(EngineKeyFunctions.MoveUp); ghost.AddFunction(EngineKeyFunctions.MoveUp);

View File

@@ -39,6 +39,11 @@ namespace Content.Client.UserInterface
private static readonly Regex Whitespace = new Regex(@"\s+", RegexOptions.Compiled); private static readonly Regex Whitespace = new Regex(@"\s+", RegexOptions.Compiled);
private static readonly BaseActionPrototype[] EmptyActionList = Array.Empty<BaseActionPrototype>(); private static readonly BaseActionPrototype[] EmptyActionList = Array.Empty<BaseActionPrototype>();
/// <summary>
/// Is an action currently being dragged from this window?
/// </summary>
public bool IsDragging => _dragDropHelper.IsDragging;
// parallel list of actions currently selectable in itemList // parallel list of actions currently selectable in itemList
private BaseActionPrototype[] _actionList; private BaseActionPrototype[] _actionList;
@@ -158,6 +163,7 @@ namespace Content.Client.UserInterface
protected override void ExitedTree() protected override void ExitedTree()
{ {
base.ExitedTree(); base.ExitedTree();
_dragDropHelper.EndDrag();
_clearButton.OnPressed -= OnClearButtonPressed; _clearButton.OnPressed -= OnClearButtonPressed;
_searchBar.OnTextChanged -= OnSearchTextChanged; _searchBar.OnTextChanged -= OnSearchTextChanged;
_filterButton.OnItemSelected -= OnFilterItemSelected; _filterButton.OnItemSelected -= OnFilterItemSelected;

View File

@@ -39,11 +39,10 @@ namespace Content.Client.UserInterface
private readonly TextureButton _lockButton; private readonly TextureButton _lockButton;
private readonly TextureButton _settingsButton; private readonly TextureButton _settingsButton;
private readonly TextureButton _previousHotbarButton;
private readonly Label _loadoutNumber; private readonly Label _loadoutNumber;
private readonly TextureButton _nextHotbarButton;
private readonly Texture _lockTexture; private readonly Texture _lockTexture;
private readonly Texture _unlockTexture; private readonly Texture _unlockTexture;
private readonly HBoxContainer _loadoutContainer;
private readonly TextureRect _dragShadow; private readonly TextureRect _dragShadow;
@@ -148,38 +147,39 @@ namespace Content.Client.UserInterface
}; };
hotbarContainer.AddChild(_slotContainer); 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 }); _loadoutContainer.AddChild(new Control { SizeFlagsHorizontal = SizeFlags.FillExpand, SizeFlagsStretchRatio = 1 });
_previousHotbarButton = new TextureButton 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, SizeFlagsHorizontal = SizeFlags.ShrinkCenter,
SizeFlagsVertical = SizeFlags.ShrinkCenter, SizeFlagsVertical = SizeFlags.ShrinkCenter,
SizeFlagsStretchRatio = 1 SizeFlagsStretchRatio = 1
}; };
loadoutContainer.AddChild(_previousHotbarButton); _loadoutContainer.AddChild(previousHotbarIcon);
loadoutContainer.AddChild(new Control { SizeFlagsHorizontal = SizeFlags.FillExpand, SizeFlagsStretchRatio = 2 }); _loadoutContainer.AddChild(new Control { SizeFlagsHorizontal = SizeFlags.FillExpand, SizeFlagsStretchRatio = 2 });
_loadoutNumber = new Label _loadoutNumber = new Label
{ {
Text = "1", Text = "1",
SizeFlagsStretchRatio = 1 SizeFlagsStretchRatio = 1
}; };
loadoutContainer.AddChild(_loadoutNumber); _loadoutContainer.AddChild(_loadoutNumber);
loadoutContainer.AddChild(new Control { SizeFlagsHorizontal = SizeFlags.FillExpand, SizeFlagsStretchRatio = 2 }); _loadoutContainer.AddChild(new Control { SizeFlagsHorizontal = SizeFlags.FillExpand, SizeFlagsStretchRatio = 2 });
_nextHotbarButton = new TextureButton 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, SizeFlagsHorizontal = SizeFlags.ShrinkCenter,
SizeFlagsVertical = SizeFlags.ShrinkCenter, SizeFlagsVertical = SizeFlags.ShrinkCenter,
SizeFlagsStretchRatio = 1 SizeFlagsStretchRatio = 1
}; };
loadoutContainer.AddChild(_nextHotbarButton); _loadoutContainer.AddChild(nextHotbarIcon);
loadoutContainer.AddChild(new Control { SizeFlagsHorizontal = SizeFlags.FillExpand, SizeFlagsStretchRatio = 1 }); _loadoutContainer.AddChild(new Control { SizeFlagsHorizontal = SizeFlags.FillExpand, SizeFlagsStretchRatio = 1 });
_slots = new ActionSlot[ClientActionsComponent.Slots]; _slots = new ActionSlot[ClientActionsComponent.Slots];
@@ -194,7 +194,7 @@ namespace Content.Client.UserInterface
for (byte i = 0; i < ClientActionsComponent.Slots; i++) 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); _slotContainer.AddChild(slot);
_slots[i] = slot; _slots[i] = slot;
} }
@@ -206,9 +206,8 @@ namespace Content.Client.UserInterface
{ {
base.EnteredTree(); base.EnteredTree();
_lockButton.OnPressed += OnLockPressed; _lockButton.OnPressed += OnLockPressed;
_nextHotbarButton.OnPressed += NextHotbar;
_previousHotbarButton.OnPressed += PreviousHotbar;
_settingsButton.OnPressed += OnToggleActionsMenu; _settingsButton.OnPressed += OnToggleActionsMenu;
_loadoutContainer.OnKeyBindDown += OnHotbarPaginate;
} }
protected override void ExitedTree() protected override void ExitedTree()
@@ -217,9 +216,8 @@ namespace Content.Client.UserInterface
StopTargeting(); StopTargeting();
_menu.Close(); _menu.Close();
_lockButton.OnPressed -= OnLockPressed; _lockButton.OnPressed -= OnLockPressed;
_nextHotbarButton.OnPressed -= NextHotbar;
_previousHotbarButton.OnPressed -= PreviousHotbar;
_settingsButton.OnPressed -= OnToggleActionsMenu; _settingsButton.OnPressed -= OnToggleActionsMenu;
_loadoutContainer.OnKeyBindDown -= OnHotbarPaginate;
} }
protected override Vector2 CalculateMinimumSize() 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; // rather than clicking the arrows themselves, the user can click the hbox so it's more
ChangeHotbar((byte) newBar); // "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) private void ChangeHotbar(byte hotbar)
{ {
@@ -547,6 +552,15 @@ namespace Content.Client.UserInterface
actionSlot.Depress(args.State == BoundKeyState.Down); actionSlot.Depress(args.State == BoundKeyState.Down);
} }
/// <summary>
/// Handle hotbar change.
/// </summary>
/// <param name="hotbar">hotbar index to switch to</param>
public void HandleChangeHotbarKeybind(byte hotbar, PointerInputCmdHandler.PointerInputCmdArgs args)
{
ChangeHotbar(hotbar);
}
protected override void FrameUpdate(FrameEventArgs args) protected override void FrameUpdate(FrameEventArgs args)
{ {
base.Update(args); base.Update(args);

View File

@@ -110,6 +110,7 @@ namespace Content.Client.UserInterface.Controls
private readonly SpriteView _bigItemSpriteView; private readonly SpriteView _bigItemSpriteView;
private readonly CooldownGraphic _cooldownGraphic; private readonly CooldownGraphic _cooldownGraphic;
private readonly ActionsUI _actionsUI; private readonly ActionsUI _actionsUI;
private readonly ActionMenu _actionMenu;
private readonly ClientActionsComponent _actionsComponent; private readonly ClientActionsComponent _actionsComponent;
private bool _toggledOn; private bool _toggledOn;
// whether button is currently pressed down by mouse or keybind down. // 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 /// Creates an action slot for the specified number
/// </summary> /// </summary>
/// <param name="slotIndex">slot index this corresponds to, 0-9 (0 labeled as 1, 8, labeled "9", 9 labeled as "0".</param> /// <param name="slotIndex">slot index this corresponds to, 0-9 (0 labeled as 1, 8, labeled "9", 9 labeled as "0".</param>
public ActionSlot(ActionsUI actionsUI, ClientActionsComponent actionsComponent, byte slotIndex) public ActionSlot(ActionsUI actionsUI, ActionMenu actionMenu, ClientActionsComponent actionsComponent, byte slotIndex)
{ {
_actionsComponent = actionsComponent; _actionsComponent = actionsComponent;
_actionsUI = actionsUI; _actionsUI = actionsUI;
_actionMenu = actionMenu;
_gameTiming = IoCManager.Resolve<IGameTiming>(); _gameTiming = IoCManager.Resolve<IGameTiming>();
SlotIndex = slotIndex; SlotIndex = slotIndex;
MouseFilter = MouseFilterMode.Stop; MouseFilter = MouseFilterMode.Stop;
@@ -259,7 +261,7 @@ namespace Content.Client.UserInterface.Controls
if (args.Function == EngineKeyFunctions.UIRightClick) 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); _actionsComponent.Assignments.ClearSlot(_actionsUI.SelectedHotbar, SlotIndex, true);
_actionsUI.StopTargeting(); _actionsUI.StopTargeting();
@@ -582,6 +584,18 @@ namespace Content.Client.UserInterface.Controls
private void DrawModeChanged() 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 // always show the normal empty button style if no action in this slot
if (!HasAssignment) if (!HasAssignment)
{ {
@@ -597,15 +611,6 @@ namespace Content.Client.UserInterface.Controls
return; 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 it's toggled on, always show the toggled on style (currently same as depressed style)
if (ToggledOn) if (ToggledOn)

View File

@@ -180,6 +180,15 @@ namespace Content.Client.UserInterface
AddButton(ContentKeyFunctions.Hotbar8, "Hotbar slot 8"); AddButton(ContentKeyFunctions.Hotbar8, "Hotbar slot 8");
AddButton(ContentKeyFunctions.Hotbar9, "Hotbar slot 9"); AddButton(ContentKeyFunctions.Hotbar9, "Hotbar slot 9");
AddButton(ContentKeyFunctions.Hotbar0, "Hotbar slot 0"); 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"); AddHeader("Map Editor");
AddButton(EngineKeyFunctions.EditorPlaceObject, "Place object"); AddButton(EngineKeyFunctions.EditorPlaceObject, "Place object");

View File

@@ -106,6 +106,15 @@ Hotbar slot 7: [color=#a4885c]{40}[/color]
Hotbar slot 8: [color=#a4885c]{41}[/color] Hotbar slot 8: [color=#a4885c]{41}[/color]
Hotbar slot 9: [color=#a4885c]{42}[/color] Hotbar slot 9: [color=#a4885c]{42}[/color]
Hotbar slot 0: [color=#a4885c]{43}[/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(MoveUp), Key(MoveLeft), Key(MoveDown), Key(MoveRight),
Key(SwapHands), Key(SwapHands),
@@ -147,7 +156,16 @@ Hotbar slot 0: [color=#a4885c]{43}[/color]
Key(Hotbar7), Key(Hotbar7),
Key(Hotbar8), Key(Hotbar8),
Key(Hotbar9), 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 //Gameplay
VBox.AddChild(new Label { FontOverride = headerFont, Text = "\nGameplay" }); VBox.AddChild(new Label { FontOverride = headerFont, Text = "\nGameplay" });

View File

@@ -1,19 +1,22 @@
using System; using System.Collections.Generic;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Content.Client.GameObjects.Components.Mobs; using Content.Client.GameObjects.Components.Mobs;
using Content.Client.UserInterface; 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.Server.GameObjects.Components.Mobs;
using Content.Shared.Actions; using Content.Shared.Actions;
using Content.Shared.Alert;
using Content.Shared.GameObjects.Components.Mobs; using Content.Shared.GameObjects.Components.Mobs;
using Content.Shared.GameObjects.EntitySystems;
using NUnit.Framework; using NUnit.Framework;
using Robust.Client.Interfaces.UserInterface; using Robust.Client.Interfaces.UserInterface;
using Robust.Client.Player; 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 namespace Content.IntegrationTests.Tests.GameObjects.Components.Mobs
{ {
@@ -21,8 +24,39 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components.Mobs
[TestOf(typeof(SharedActionsComponent))] [TestOf(typeof(SharedActionsComponent))]
[TestOf(typeof(ClientActionsComponent))] [TestOf(typeof(ClientActionsComponent))]
[TestOf(typeof(ServerActionsComponent))] [TestOf(typeof(ServerActionsComponent))]
[TestOf(typeof(ItemActionsComponent))]
public class ActionsComponentTests : ContentIntegrationTest 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] [Test]
public async Task GrantsAndRevokesActionsTest() 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(actionsComponent.TryGetActionState(innateAction, out var innateState));
Assert.That(innateState.Enabled); Assert.That(innateState.Enabled);
} }
Assert.That(innateActions.Count, Is.GreaterThan(0));
actionsComponent.Grant(ActionType.DebugInstant); actionsComponent.Grant(ActionType.DebugInstant);
Assert.That(actionsComponent.TryGetActionState(ActionType.HumanScream, out var state) && state.Enabled); 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<Robust.Server.Interfaces.Player.IPlayerManager>();
var serverEntManager = server.ResolveDependency<IEntityManager>();
var serverGameTiming = server.ResolveDependency<IGameTiming>();
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<ServerActionsComponent>();
// spawn and give them an item that has actions
serverFlashlight = serverEntManager.SpawnEntity("TestFlashlight",
new EntityCoordinates(new EntityUid(1), (0, 0)));
Assert.That(serverFlashlight.TryGetComponent<ItemActionsComponent>(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<HandsComponent>().PutInHand(serverFlashlight.GetComponent<ItemComponent>(), 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<IPlayerManager>();
var clientUIMgr = client.ResolveDependency<IUserInterfaceManager>();
var clientEntMgr = client.ResolveDependency<IEntityManager>();
EntityUid clientFlashlight = default;
await client.WaitAssertion(() =>
{
var local = clientPlayerMgr.LocalPlayer;
var controlled = local.ControlledEntity;
clientActionsComponent = controlled.GetComponent<ClientActionsComponent>();
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<HandsComponent>()
.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<HandsComponent>().PutInHand(serverFlashlight.GetComponent<ItemComponent>(), 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)));
});
}
} }
} }

View File

@@ -265,6 +265,10 @@ namespace Content.Shared.GameObjects.Components.Mobs
return; return;
itemStates.Remove(actionType); itemStates.Remove(actionType);
if (itemStates.Count == 0)
{
_itemActions.Remove(item);
}
AfterActionChanged(); AfterActionChanged();
Dirty(); Dirty();
} }

View File

@@ -52,5 +52,14 @@ namespace Content.Shared.Input
public static readonly BoundKeyFunction Hotbar7 = "Hotbar7"; public static readonly BoundKeyFunction Hotbar7 = "Hotbar7";
public static readonly BoundKeyFunction Hotbar8 = "Hotbar8"; public static readonly BoundKeyFunction Hotbar8 = "Hotbar8";
public static readonly BoundKeyFunction Hotbar9 = "Hotbar9"; 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";
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 310 B

After

Width:  |  Height:  |  Size: 395 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 309 B

After

Width:  |  Height:  |  Size: 449 B

View File

@@ -334,3 +334,39 @@ binds:
- function: Hotbar9 - function: Hotbar9
type: State type: State
key: Num9 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