diff --git a/Content.Client/GameObjects/Components/GUI/StrippableComponent.cs b/Content.Client/GameObjects/Components/GUI/StrippableComponent.cs new file mode 100644 index 0000000000..6fa50ded40 --- /dev/null +++ b/Content.Client/GameObjects/Components/GUI/StrippableComponent.cs @@ -0,0 +1,22 @@ +using Content.Client.GameObjects.Components.Items; +using Content.Client.Interfaces.GameObjects.Components.Interaction; +using Content.Shared.GameObjects.Components.GUI; +using Robust.Shared.GameObjects; + +namespace Content.Client.GameObjects.Components.GUI +{ + [RegisterComponent] + public class StrippableComponent : SharedStrippableComponent, IClientDraggable + { + public bool ClientCanDropOn(CanDropEventArgs eventArgs) + { + return eventArgs.Target.HasComponent() + && eventArgs.Target != eventArgs.Dragged && eventArgs.Target == eventArgs.User; + } + + public bool ClientCanDrag(CanDragEventArgs eventArgs) + { + return true; + } + } +} diff --git a/Content.Client/GameObjects/Components/HUD/Inventory/StrippableBoundUserInterface.cs b/Content.Client/GameObjects/Components/HUD/Inventory/StrippableBoundUserInterface.cs new file mode 100644 index 0000000000..afd87e0e06 --- /dev/null +++ b/Content.Client/GameObjects/Components/HUD/Inventory/StrippableBoundUserInterface.cs @@ -0,0 +1,81 @@ +using System.Collections.Generic; +using Content.Client.UserInterface; +using Content.Shared.GameObjects.Components.GUI; +using Content.Shared.GameObjects.Components.Inventory; +using JetBrains.Annotations; +using Robust.Client.GameObjects.Components.UserInterface; +using Robust.Shared.GameObjects.Components.UserInterface; +using Robust.Shared.ViewVariables; +using static Content.Shared.GameObjects.Components.Inventory.EquipmentSlotDefines; + +namespace Content.Client.GameObjects.Components.HUD.Inventory +{ + [UsedImplicitly] + public class StrippableBoundUserInterface : BoundUserInterface + { + public Dictionary Inventory { get; private set; } + public Dictionary Hands { get; private set; } + + [ViewVariables] + private StrippingMenu _strippingMenu; + + public StrippableBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) + { + } + + protected override void Open() + { + base.Open(); + + _strippingMenu = new StrippingMenu($"{Owner.Owner.Name}'s inventory"); + _strippingMenu.OpenCentered(); + UpdateMenu(); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + if (!disposing) return; + _strippingMenu.Dispose(); + + _strippingMenu.Close(); + } + + private void UpdateMenu() + { + if (_strippingMenu == null) return; + + _strippingMenu.ClearButtons(); + + if(Inventory != null) + foreach (var (slot, name) in Inventory) + { + _strippingMenu.AddButton(EquipmentSlotDefines.SlotNames[slot], name, (ev) => + { + SendMessage(new StrippingInventoryButtonPressed(slot)); + }); + } + + if(Hands != null) + foreach (var (hand, name) in Hands) + { + _strippingMenu.AddButton(hand, name, (ev) => + { + SendMessage(new StrippingHandButtonPressed(hand)); + }); + } + } + + protected override void UpdateState(BoundUserInterfaceState state) + { + base.UpdateState(state); + + if (!(state is StrippingBoundUserInterfaceState stripState)) return; + + Inventory = stripState.Inventory; + Hands = stripState.Hands; + + UpdateMenu(); + } + } +} diff --git a/Content.Client/GameObjects/Components/Items/HandsComponent.cs b/Content.Client/GameObjects/Components/Items/HandsComponent.cs index 0a024e4195..daa03d74e1 100644 --- a/Content.Client/GameObjects/Components/Items/HandsComponent.cs +++ b/Content.Client/GameObjects/Components/Items/HandsComponent.cs @@ -23,6 +23,7 @@ namespace Content.Client.GameObjects.Components.Items [Dependency] private readonly IGameHud _gameHud = default!; #pragma warning restore 649 + /// private readonly List _hands = new List(); [ViewVariables] public IReadOnlyList Hands => _hands; diff --git a/Content.Client/UserInterface/StrippingMenu.cs b/Content.Client/UserInterface/StrippingMenu.cs new file mode 100644 index 0000000000..3088bd4270 --- /dev/null +++ b/Content.Client/UserInterface/StrippingMenu.cs @@ -0,0 +1,64 @@ +using System; +using Content.Client.UserInterface.Stylesheets; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.CustomControls; +using Robust.Shared.Map; +using Robust.Shared.Maths; + +namespace Content.Client.UserInterface +{ + public class StrippingMenu : SS14Window + { + protected override Vector2? CustomSize => new Vector2(400, 600); + + private readonly VBoxContainer _vboxContainer; + + public StrippingMenu(string title) + { + Title = title; + + _vboxContainer = new VBoxContainer() + { + SizeFlagsVertical = SizeFlags.FillExpand, + SeparationOverride = 5, + }; + + Contents.AddChild(_vboxContainer); + } + + public void ClearButtons() + { + _vboxContainer.DisposeAllChildren(); + } + + public void AddButton(string title, string name, Action onPressed) + { + var button = new Button() + { + Text = name, + StyleClasses = { StyleBase.ButtonOpenRight } + }; + + button.OnPressed += onPressed; + + _vboxContainer.AddChild(new HBoxContainer() + { + SizeFlagsHorizontal = SizeFlags.FillExpand, + SeparationOverride = 5, + Children = + { + new Label() + { + Text = $"{title}:" + }, + new Control() + { + SizeFlagsHorizontal = SizeFlags.FillExpand + }, + button, + } + }); + } + } +} diff --git a/Content.Server/GameObjects/Components/GUI/HandsComponent.cs b/Content.Server/GameObjects/Components/GUI/HandsComponent.cs index 0ea707e19c..e9097d40b5 100644 --- a/Content.Server/GameObjects/Components/GUI/HandsComponent.cs +++ b/Content.Server/GameObjects/Components/GUI/HandsComponent.cs @@ -42,6 +42,8 @@ namespace Content.Server.GameObjects.Components.GUI private string? _activeHand; private uint _nextHand; + public event Action? OnItemChanged; + [ViewVariables(VVAccess.ReadWrite)] public string? ActiveHand { @@ -60,6 +62,8 @@ namespace Content.Server.GameObjects.Components.GUI [ViewVariables] private readonly List _hands = new List(); + public IEnumerable Hands => _hands.Select(h => h.Name); + // Mostly arbitrary. public const float PickupRange = 2; @@ -105,6 +109,12 @@ namespace Content.Server.GameObjects.Components.GUI return GetHand(handName)?.Entity?.GetComponent(); } + public bool TryGetItem(string handName, [MaybeNullWhen(false)] out ItemComponent item) + { + item = GetItem(handName); + return item != null; + } + public ItemComponent? GetActiveHand => ActiveHand == null ? null : GetItem(ActiveHand); @@ -136,6 +146,8 @@ namespace Content.Server.GameObjects.Components.GUI { if (PutInHand(item, hand, false)) { + OnItemChanged?.Invoke(); + return true; } } @@ -156,6 +168,7 @@ namespace Content.Server.GameObjects.Components.GUI if (success) { item.Owner.Transform.LocalPosition = Vector2.Zero; + OnItemChanged?.Invoke(); } _entitySystemManager.GetEntitySystem().HandSelectedInteraction(Owner, item.Owner); @@ -250,6 +263,8 @@ namespace Content.Server.GameObjects.Components.GUI container.Insert(item.Owner); } + OnItemChanged?.Invoke(); + Dirty(); return true; } @@ -300,6 +315,8 @@ namespace Content.Server.GameObjects.Components.GUI container.Insert(item.Owner); } + OnItemChanged?.Invoke(); + Dirty(); return true; } @@ -364,6 +381,8 @@ namespace Content.Server.GameObjects.Components.GUI throw new InvalidOperationException(); } + OnItemChanged?.Invoke(); + Dirty(); return true; } @@ -415,6 +434,8 @@ namespace Content.Server.GameObjects.Components.GUI ActiveHand ??= name; + OnItemChanged?.Invoke(); + Dirty(); } @@ -435,6 +456,8 @@ namespace Content.Server.GameObjects.Components.GUI _activeHand = _hands.FirstOrDefault()?.Name; } + OnItemChanged?.Invoke(); + Dirty(); } @@ -645,7 +668,7 @@ namespace Content.Server.GameObjects.Components.GUI Dirty(); - if (!message.Entity.TryGetComponent(out IPhysicsComponent physics)) + if (!message.Entity.TryGetComponent(out ICollidableComponent physics)) { return; } diff --git a/Content.Server/GameObjects/Components/GUI/InventoryComponent.cs b/Content.Server/GameObjects/Components/GUI/InventoryComponent.cs index aa9f03ac10..1d1e52b6a2 100644 --- a/Content.Server/GameObjects/Components/GUI/InventoryComponent.cs +++ b/Content.Server/GameObjects/Components/GUI/InventoryComponent.cs @@ -33,9 +33,13 @@ namespace Content.Server.GameObjects.Components.GUI #pragma warning restore 649 [ViewVariables] - private readonly Dictionary SlotContainers = new Dictionary(); + private readonly Dictionary _slotContainers = new Dictionary(); - private KeyValuePair? HoverEntity; + private KeyValuePair? _hoverEntity; + + public IEnumerable Slots => _slotContainers.Keys; + + public event Action OnItemChanged; public override void Initialize() { @@ -43,7 +47,7 @@ namespace Content.Server.GameObjects.Components.GUI foreach (var slotName in InventoryInstance.SlotMasks) { - if (slotName != Slots.NONE) + if (slotName != EquipmentSlotDefines.Slots.NONE) { AddSlot(slotName); } @@ -58,7 +62,7 @@ namespace Content.Server.GameObjects.Components.GUI { var multiplier = 1f; - foreach (var (slot, containerSlot) in SlotContainers) + foreach (var (slot, containerSlot) in _slotContainers) { foreach (var entity in containerSlot.ContainedEntities) { @@ -81,7 +85,7 @@ namespace Content.Server.GameObjects.Components.GUI { var multiplier = 1f; - foreach (var (slot, containerSlot) in SlotContainers) + foreach (var (slot, containerSlot) in _slotContainers) { foreach (var entity in containerSlot.ContainedEntities) { @@ -99,7 +103,7 @@ namespace Content.Server.GameObjects.Components.GUI bool IEffectBlocker.CanSlip() { if(Owner.TryGetComponent(out InventoryComponent inventoryComponent) && - inventoryComponent.TryGetSlotItem(Slots.SHOES, out ItemComponent shoes) + inventoryComponent.TryGetSlotItem(EquipmentSlotDefines.Slots.SHOES, out ItemComponent shoes) ) { return EffectBlockerSystem.CanSlip(shoes.Owner); @@ -110,7 +114,7 @@ namespace Content.Server.GameObjects.Components.GUI public override void OnRemove() { - var slots = SlotContainers.Keys.ToList(); + var slots = _slotContainers.Keys.ToList(); foreach (var slot in slots) { RemoveSlot(slot); @@ -140,15 +144,15 @@ namespace Content.Server.GameObjects.Components.GUI } public T GetSlotItem(Slots slot) where T : ItemComponent { - if (!SlotContainers.ContainsKey(slot)) + if (!_slotContainers.ContainsKey(slot)) { return null; } - var containedEntity = SlotContainers[slot].ContainedEntity; + var containedEntity = _slotContainers[slot].ContainedEntity; if (containedEntity?.Deleted == true) { - SlotContainers[slot] = null; + _slotContainers[slot] = null; containedEntity = null; Dirty(); } @@ -169,7 +173,7 @@ namespace Content.Server.GameObjects.Components.GUI /// /// The slot to put the item in. /// The item to insert into the slot. - /// The translated reason why the item cannot be equiped, if this function returns false. Can be null. + /// The translated reason why the item cannot be equipped, if this function returns false. Can be null. /// True if the item was successfully inserted, false otherwise. public bool Equip(Slots slot, ItemComponent item, out string reason) { @@ -184,7 +188,7 @@ namespace Content.Server.GameObjects.Components.GUI return false; } - var inventorySlot = SlotContainers[slot]; + var inventorySlot = _slotContainers[slot]; if (!inventorySlot.Insert(item.Owner)) { return false; @@ -192,6 +196,8 @@ namespace Content.Server.GameObjects.Components.GUI _entitySystemManager.GetEntitySystem().EquippedInteraction(Owner, item.Owner, slot); + OnItemChanged?.Invoke(); + Dirty(); return true; @@ -239,7 +245,7 @@ namespace Content.Server.GameObjects.Components.GUI reason = Loc.GetString("You can't equip this!"); } - return pass && SlotContainers[slot].CanInsert(item.Owner); + return pass && _slotContainers[slot].CanInsert(item.Owner); } public bool CanEquip(Slots slot, ItemComponent item) => CanEquip(slot, item, out var _); @@ -258,7 +264,7 @@ namespace Content.Server.GameObjects.Components.GUI return false; } - var inventorySlot = SlotContainers[slot]; + var inventorySlot = _slotContainers[slot]; var item = inventorySlot.ContainedEntity.GetComponent(); if (!inventorySlot.Remove(inventorySlot.ContainedEntity)) { @@ -271,6 +277,8 @@ namespace Content.Server.GameObjects.Components.GUI _entitySystemManager.GetEntitySystem().UnequippedInteraction(Owner, item.Owner, slot); + OnItemChanged?.Invoke(); + Dirty(); return true; @@ -288,7 +296,7 @@ namespace Content.Server.GameObjects.Components.GUI if (!ActionBlockerSystem.CanUnequip(Owner)) return false; - var InventorySlot = SlotContainers[slot]; + var InventorySlot = _slotContainers[slot]; return InventorySlot.ContainedEntity != null && InventorySlot.CanRemove(InventorySlot.ContainedEntity); } @@ -307,7 +315,12 @@ namespace Content.Server.GameObjects.Components.GUI } Dirty(); - return SlotContainers[slot] = ContainerManagerComponent.Create(GetSlotString(slot), Owner); + + _slotContainers[slot] = ContainerManagerComponent.Create(GetSlotString(slot), Owner); + + OnItemChanged?.Invoke(); + + return _slotContainers[slot]; } /// @@ -331,7 +344,10 @@ namespace Content.Server.GameObjects.Components.GUI "Unable to remove slot as the contained clothing could not be dropped"); } - SlotContainers.Remove(slot); + _slotContainers.Remove(slot); + + OnItemChanged?.Invoke(); + Dirty(); } @@ -342,7 +358,7 @@ namespace Content.Server.GameObjects.Components.GUI /// True if the slot exists, false otherwise. public bool HasSlot(Slots slot) { - return SlotContainers.ContainsKey(slot); + return _slotContainers.ContainsKey(slot); } /// @@ -354,7 +370,7 @@ namespace Content.Server.GameObjects.Components.GUI // make sure this is one of our containers. // Technically the correct way would be to enumerate the possible slot names // comparing with this container, but I might as well put the dictionary to good use. - if (!(container is ContainerSlot slot) || !SlotContainers.ContainsValue(slot)) + if (!(container is ContainerSlot slot) || !_slotContainers.ContainsValue(slot)) return; if (entity.TryGetComponent(out ItemComponent itemComp)) @@ -362,6 +378,8 @@ namespace Content.Server.GameObjects.Components.GUI itemComp.RemovedFromSlot(); } + OnItemChanged?.Invoke(); + Dirty(); } @@ -417,7 +435,7 @@ namespace Content.Server.GameObjects.Components.GUI if (activeHand != null && GetSlotItem(msg.Inventoryslot) == null) { var canEquip = CanEquip(msg.Inventoryslot, activeHand, out var reason); - HoverEntity = new KeyValuePair(msg.Inventoryslot, (activeHand.Owner.Uid, canEquip)); + _hoverEntity = new KeyValuePair(msg.Inventoryslot, (activeHand.Owner.Uid, canEquip)); Dirty(); } @@ -476,7 +494,7 @@ namespace Content.Server.GameObjects.Components.GUI public override ComponentState GetComponentState() { var list = new List>(); - foreach (var (slot, container) in SlotContainers) + foreach (var (slot, container) in _slotContainers) { if (container.ContainedEntity != null) { @@ -484,8 +502,8 @@ namespace Content.Server.GameObjects.Components.GUI } } - var hover = HoverEntity; - HoverEntity = null; + var hover = _hoverEntity; + _hoverEntity = null; return new InventoryComponentState(list, hover); } @@ -497,7 +515,7 @@ namespace Content.Server.GameObjects.Components.GUI return; } - foreach (var slot in SlotContainers.Values.ToList()) + foreach (var slot in _slotContainers.Values.ToList()) { foreach (var entity in slot.ContainedEntities) { diff --git a/Content.Server/GameObjects/Components/GUI/StrippableComponent.cs b/Content.Server/GameObjects/Components/GUI/StrippableComponent.cs new file mode 100644 index 0000000000..e045fd2b5a --- /dev/null +++ b/Content.Server/GameObjects/Components/GUI/StrippableComponent.cs @@ -0,0 +1,372 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using Content.Server.GameObjects.Components.Items.Storage; +using Content.Server.GameObjects.EntitySystems.DoAfter; +using Content.Server.Interfaces; +using Content.Shared.GameObjects.Components.GUI; +using Content.Shared.GameObjects.Components.Inventory; +using Content.Shared.GameObjects.EntitySystems; +using Content.Shared.Interfaces.GameObjects.Components; +using Robust.Server.GameObjects.Components.UserInterface; +using Robust.Server.Interfaces.GameObjects; +using Robust.Server.Interfaces.Player; +using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Systems; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Localization; +using Robust.Shared.Log; +using Robust.Shared.ViewVariables; +using static Content.Shared.GameObjects.Components.Inventory.EquipmentSlotDefines; + +namespace Content.Server.GameObjects.Components.GUI +{ + [RegisterComponent] + public sealed class StrippableComponent : SharedStrippableComponent, IDragDrop + { + [Dependency] private IServerNotifyManager _notifyManager = default!; + + public const float StripDelay = 2f; + + [ViewVariables] + private BoundUserInterface _userInterface; + + private InventoryComponent _inventoryComponent; + private HandsComponent _handsComponent; + + public override void Initialize() + { + base.Initialize(); + + _userInterface = Owner.GetComponent().GetBoundUserInterface(StrippingUiKey.Key); + _userInterface.OnReceiveMessage += HandleUserInterfaceMessage; + + _inventoryComponent = Owner.GetComponent(); + _handsComponent = Owner.GetComponent(); + + _inventoryComponent.OnItemChanged += UpdateSubscribed; + + // Initial update. + UpdateSubscribed(); + } + + private void UpdateSubscribed() + { + var inventory = GetInventorySlots(); + var hands = GetHandSlots(); + + _userInterface.SetState(new StrippingBoundUserInterfaceState(inventory, hands)); + } + + public bool CanDragDrop(DragDropEventArgs eventArgs) + { + return eventArgs.User.HasComponent() + && eventArgs.Target != eventArgs.Dropped && eventArgs.Target == eventArgs.User; + } + + public bool DragDrop(DragDropEventArgs eventArgs) + { + if (!eventArgs.User.TryGetComponent(out IActorComponent actor)) return false; + + OpenUserInterface(actor.playerSession); + return true; + } + + private Dictionary GetInventorySlots() + { + var dictionary = new Dictionary(); + + foreach (var slot in _inventoryComponent.Slots) + { + dictionary[slot] = _inventoryComponent.GetSlotItem(slot)?.Owner.Name ?? "None"; + } + + return dictionary; + } + + private Dictionary GetHandSlots() + { + var dictionary = new Dictionary(); + + foreach (var hand in _handsComponent.Hands) + { + dictionary[hand] = _handsComponent.GetItem(hand)?.Owner.Name ?? "None"; + } + + return dictionary; + } + + private void OpenUserInterface(IPlayerSession session) + { + _userInterface.Open(session); + } + + /// + /// Places item in user's active hand to an inventory slot. + /// + private async void PlaceActiveHandItemInInventory(IEntity user, Slots slot) + { + var inventory = Owner.GetComponent(); + var userHands = user.GetComponent(); + var item = userHands.GetActiveHand; + + bool Check() + { + if (!ActionBlockerSystem.CanInteract(user)) + return false; + + if (item == null) + { + _notifyManager.PopupMessageCursor(user, Loc.GetString("You aren't holding anything!")); + return false; + } + + if (!userHands.CanDrop(userHands.ActiveHand!)) + { + _notifyManager.PopupMessageCursor(user, Loc.GetString("You can't drop that!")); + return false; + } + + if (!inventory.HasSlot(slot)) + return false; + + if (inventory.TryGetSlotItem(slot, out ItemComponent _)) + { + _notifyManager.PopupMessageCursor(user, Loc.GetString("{0:They} already {0:have} something there!", Owner)); + return false; + } + + if (!inventory.CanEquip(slot, item)) + { + _notifyManager.PopupMessageCursor(user, Loc.GetString("{0:They} cannot equip that there!", Owner)); + return false; + } + + return true; + } + + var doAfterSystem = EntitySystem.Get(); + + var doAfterArgs = new DoAfterEventArgs(user, StripDelay, CancellationToken.None, Owner) + { + ExtraCheck = Check, + BreakOnStun = true, + BreakOnDamage = true, + BreakOnTargetMove = true, + BreakOnUserMove = true, + NeedHand = true, + }; + + var result = await doAfterSystem.DoAfter(doAfterArgs); + if (result != DoAfterStatus.Finished) return; + + userHands.Drop(item!.Owner, false); + inventory.Equip(slot, item!.Owner); + + UpdateSubscribed(); + } + + /// + /// Places item in user's active hand in one of the entity's hands. + /// + private async void PlaceActiveHandItemInHands(IEntity user, string hand) + { + var hands = Owner.GetComponent(); + var userHands = user.GetComponent(); + var item = userHands.GetActiveHand; + + bool Check() + { + if (!ActionBlockerSystem.CanInteract(user)) + return false; + + if (item == null) + { + _notifyManager.PopupMessageCursor(user, Loc.GetString("You aren't holding anything!")); + return false; + } + + if (!userHands.CanDrop(userHands.ActiveHand!)) + { + _notifyManager.PopupMessageCursor(user, Loc.GetString("You can't drop that!")); + return false; + } + + if (!hands.HasHand(hand)) + return false; + + if (hands.TryGetItem(hand, out var _)) + { + _notifyManager.PopupMessageCursor(user, Loc.GetString("{0:They} already {0:have} something there!", Owner)); + return false; + } + + if (!hands.CanPutInHand(item, hand)) + { + _notifyManager.PopupMessageCursor(user, Loc.GetString("{0:They} cannot put that there!", Owner)); + return false; + } + + return true; + } + + var doAfterSystem = EntitySystem.Get(); + + var doAfterArgs = new DoAfterEventArgs(user, StripDelay, CancellationToken.None, Owner) + { + ExtraCheck = Check, + BreakOnStun = true, + BreakOnDamage = true, + BreakOnTargetMove = true, + BreakOnUserMove = true, + NeedHand = true, + }; + + var result = await doAfterSystem.DoAfter(doAfterArgs); + if (result != DoAfterStatus.Finished) return; + + userHands.Drop(hand, false); + hands.PutInHand(item, hand, false); + UpdateSubscribed(); + } + + /// + /// Takes an item from the inventory and places it in the user's active hand. + /// + private async void TakeItemFromInventory(IEntity user, Slots slot) + { + var inventory = Owner.GetComponent(); + var userHands = user.GetComponent(); + + bool Check() + { + if (!ActionBlockerSystem.CanInteract(user)) + return false; + + if (!inventory.HasSlot(slot)) + return false; + + if (!inventory.TryGetSlotItem(slot, out ItemComponent itemToTake)) + { + _notifyManager.PopupMessageCursor(user, Loc.GetString("{0:They} {0:have} nothing there!", Owner)); + return false; + } + + if (!inventory.CanUnequip(slot)) + { + _notifyManager.PopupMessageCursor(user, Loc.GetString("{0:They} cannot unequip that!", Owner)); + return false; + } + + return true; + } + + var doAfterSystem = EntitySystem.Get(); + + var doAfterArgs = new DoAfterEventArgs(user, StripDelay, CancellationToken.None, Owner) + { + ExtraCheck = Check, + BreakOnStun = true, + BreakOnDamage = true, + BreakOnTargetMove = true, + BreakOnUserMove = true, + }; + + var result = await doAfterSystem.DoAfter(doAfterArgs); + if (result != DoAfterStatus.Finished) return; + + var item = inventory.GetSlotItem(slot); + inventory.Unequip(slot); + userHands.PutInHandOrDrop(item); + UpdateSubscribed(); + } + + /// + /// Takes an item from a hand and places it in the user's active hand. + /// + private async void TakeItemFromHands(IEntity user, string hand) + { + var hands = Owner.GetComponent(); + var userHands = user.GetComponent(); + + bool Check() + { + if (!ActionBlockerSystem.CanInteract(user)) + return false; + + if (!hands.HasHand(hand)) + return false; + + if (!hands.TryGetItem(hand, out var heldItem)) + { + _notifyManager.PopupMessageCursor(user, Loc.GetString("{0:They} {0:have} nothing there!", Owner)); + return false; + } + + if (!hands.CanDrop(hand)) + { + _notifyManager.PopupMessageCursor(user, Loc.GetString("{0:They} cannot drop that!", Owner)); + return false; + } + + return true; + } + + var doAfterSystem = EntitySystem.Get(); + + var doAfterArgs = new DoAfterEventArgs(user, StripDelay, CancellationToken.None, Owner) + { + ExtraCheck = Check, + BreakOnStun = true, + BreakOnDamage = true, + BreakOnTargetMove = true, + BreakOnUserMove = true, + }; + + var result = await doAfterSystem.DoAfter(doAfterArgs); + if (result != DoAfterStatus.Finished) return; + + var item = hands.GetItem(hand); + hands.Drop(hand, false); + userHands.PutInHandOrDrop(item); + UpdateSubscribed(); + } + + private void HandleUserInterfaceMessage(ServerBoundUserInterfaceMessage obj) + { + var user = obj.Session.AttachedEntity; + if (user == null || !(user.TryGetComponent(out HandsComponent userHands))) return; + + var placingItem = userHands.GetActiveHand != null; + + switch (obj.Message) + { + case StrippingInventoryButtonPressed inventoryMessage: + var inventory = Owner.GetComponent(); + + if (inventory.TryGetSlotItem(inventoryMessage.Slot, out ItemComponent _)) + placingItem = false; + + if(placingItem) + PlaceActiveHandItemInInventory(user, inventoryMessage.Slot); + else + TakeItemFromInventory(user, inventoryMessage.Slot); + break; + case StrippingHandButtonPressed handMessage: + var hands = Owner.GetComponent(); + + if (hands.TryGetItem(handMessage.Hand, out _)) + placingItem = false; + + if(placingItem) + PlaceActiveHandItemInHands(user, handMessage.Hand); + else + TakeItemFromHands(user, handMessage.Hand); + break; + default: + break; + } + } + } +} diff --git a/Content.Server/GameObjects/Components/Mobs/MindComponent.cs b/Content.Server/GameObjects/Components/Mobs/MindComponent.cs index 20271f0b23..d148490bd9 100644 --- a/Content.Server/GameObjects/Components/Mobs/MindComponent.cs +++ b/Content.Server/GameObjects/Components/Mobs/MindComponent.cs @@ -132,12 +132,12 @@ namespace Content.Server.GameObjects.Components.Mobs if (!HasMind) { message.AddMarkup(!dead - ? $"[color=red]" + Loc.GetString("{0:They} are totally catatonic. The stresses of life in deep-space must have been too much for {0:them}. Any recovery is unlikely.", Owner) + "[/color]" + ? $"[color=red]" + Loc.GetString("{0:They} {0:are} totally catatonic. The stresses of life in deep-space must have been too much for {0:them}. Any recovery is unlikely.", Owner) + "[/color]" : $"[color=purple]" + Loc.GetString("{0:Their} soul has departed.", Owner) + "[/color]"); } else if (Mind?.Session == null) { - message.AddMarkup("[color=yellow]" + Loc.GetString("{0:They} have a blank, absent-minded stare and appears completely unresponsive to anything. {0:They} may snap out of it soon.", Owner) + "[/color]"); + message.AddMarkup("[color=yellow]" + Loc.GetString("{0:They} {0:have} a blank, absent-minded stare and appears completely unresponsive to anything. {0:They} may snap out of it soon.", Owner) + "[/color]"); } } } diff --git a/Content.Server/Interfaces/GameObjects/Components/Items/IHandsComponent.cs b/Content.Server/Interfaces/GameObjects/Components/Items/IHandsComponent.cs index d631716cc9..e716533285 100644 --- a/Content.Server/Interfaces/GameObjects/Components/Items/IHandsComponent.cs +++ b/Content.Server/Interfaces/GameObjects/Components/Items/IHandsComponent.cs @@ -1,6 +1,11 @@ -using System; +#nullable enable +using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Content.Server.GameObjects; +using Content.Server.GameObjects.Components.GUI; using Content.Server.GameObjects.Components.Items.Storage; +using Content.Shared.GameObjects.Components.Inventory; using Content.Shared.GameObjects.Components.Items; using Content.Shared.GameObjects.EntitySystems; using Robust.Server.GameObjects.Components.Container; @@ -12,10 +17,20 @@ namespace Content.Server.Interfaces.GameObjects.Components.Items { public interface IHandsComponent : ISharedHandsComponent { + /// + /// Invoked when the hand contents changes or when a hand is added/removed. + /// + event Action? OnItemChanged; + + /// + /// The hands in this component. + /// + IEnumerable Hands { get; } + /// /// The hand name of the currently active hand. /// - string ActiveHand { get; set; } + string? ActiveHand { get; set; } /// /// Enumerates over every held item. @@ -27,12 +42,20 @@ namespace Content.Server.Interfaces.GameObjects.Components.Items /// /// The name of the hand to get. /// The item in the held, null if no item is held - ItemComponent GetItem(string handName); + ItemComponent? GetItem(string handName); + + /// + /// Attempts to get an item in a hand. + /// + /// The name of the hand to get. + /// The item in the held, null if no item is held + /// Whether it was holding an item + bool TryGetItem(string handName, [MaybeNullWhen(false)] out ItemComponent item); /// /// Gets item held by the current active hand /// - ItemComponent GetActiveHand { get; } + ItemComponent? GetActiveHand { get; } /// /// Puts an item into any empty hand, preferring the active hand. @@ -78,7 +101,7 @@ namespace Content.Server.Interfaces.GameObjects.Components.Items /// /// true if the entity is held, false otherwise /// - bool TryHand(IEntity entity, out string handName); + bool TryHand(IEntity entity, [MaybeNullWhen(false)] out string handName); /// /// Drops the item contained in the slot to the same position as our entity. diff --git a/Content.Shared/GameObjects/Components/GUI/SharedStrippableComponent.cs b/Content.Shared/GameObjects/Components/GUI/SharedStrippableComponent.cs new file mode 100644 index 0000000000..bcc9b2fc7c --- /dev/null +++ b/Content.Shared/GameObjects/Components/GUI/SharedStrippableComponent.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using Content.Shared.GameObjects.Components.Inventory; +using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Components.UserInterface; +using Robust.Shared.Serialization; +using static Content.Shared.GameObjects.Components.Inventory.EquipmentSlotDefines; + +namespace Content.Shared.GameObjects.Components.GUI +{ + public class SharedStrippableComponent : Component + { + public override string Name => "Strippable"; + + [NetSerializable, Serializable] + public enum StrippingUiKey + { + Key, + } + } + + [NetSerializable, Serializable] + public class StrippingInventoryButtonPressed : BoundUserInterfaceMessage + { + public Slots Slot { get; } + + public StrippingInventoryButtonPressed(Slots slot) + { + Slot = slot; + } + } + + [NetSerializable, Serializable] + public class StrippingHandButtonPressed : BoundUserInterfaceMessage + { + public string Hand { get; } + + public StrippingHandButtonPressed(string hand) + { + Hand = hand; + } + } + + [NetSerializable, Serializable] + public class StrippingBoundUserInterfaceState : BoundUserInterfaceState + { + public Dictionary Inventory { get; } + public Dictionary Hands { get; } + + public StrippingBoundUserInterfaceState(Dictionary inventory, Dictionary hands) + { + Inventory = inventory; + Hands = hands; + } + } +} diff --git a/Resources/Prototypes/Entities/Mobs/Species/human.yml b/Resources/Prototypes/Entities/Mobs/Species/human.yml index b5a18678fd..9259c28043 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/human.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/human.yml @@ -144,6 +144,12 @@ - type: Pullable - type: CanSeeGases - type: DoAfter + - type: Strippable + - type: UserInterface + interfaces: + - key: enum.StrippingUiKey.Key + type: StrippableBoundUserInterface + - type: entity save: false diff --git a/SpaceStation14.sln.DotSettings b/SpaceStation14.sln.DotSettings index f9a53158a2..c93412bf8d 100644 --- a/SpaceStation14.sln.DotSettings +++ b/SpaceStation14.sln.DotSettings @@ -71,10 +71,12 @@ True True True + True True True True True + True True True True