Actions System + UI (#2710)
Co-authored-by: Vera Aguilera Puerto <6766154+Zumorica@users.noreply.github.com>
This commit is contained in:
@@ -12,6 +12,7 @@ using Content.Client.UserInterface;
|
||||
using Content.Client.UserInterface.AdminMenu;
|
||||
using Content.Client.UserInterface.Stylesheets;
|
||||
using Content.Client.Utility;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Interfaces;
|
||||
using Content.Shared.Alert;
|
||||
using Robust.Shared.IoC;
|
||||
@@ -39,6 +40,7 @@ namespace Content.Client
|
||||
IoCManager.Register<IStationEventManager, StationEventManager>();
|
||||
IoCManager.Register<IAdminMenuManager, AdminMenuManager>();
|
||||
IoCManager.Register<AlertManager, AlertManager>();
|
||||
IoCManager.Register<ActionManager, ActionManager>();
|
||||
IoCManager.Register<IClientAdminManager, ClientAdminManager>();
|
||||
IoCManager.Register<EuiManager, EuiManager>();
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ using Content.Client.StationEvents;
|
||||
using Content.Client.UserInterface;
|
||||
using Content.Client.UserInterface.AdminMenu;
|
||||
using Content.Client.UserInterface.Stylesheets;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.GameObjects.Components;
|
||||
using Content.Shared.GameObjects.Components.Cargo;
|
||||
using Content.Shared.GameObjects.Components.Chemistry;
|
||||
@@ -157,6 +158,7 @@ namespace Content.Client
|
||||
IoCManager.Resolve<IAdminMenuManager>().Initialize();
|
||||
IoCManager.Resolve<EuiManager>().Initialize();
|
||||
IoCManager.Resolve<AlertManager>().Initialize();
|
||||
IoCManager.Resolve<ActionManager>().Initialize();
|
||||
|
||||
_baseClient.RunLevelChanged += (sender, args) =>
|
||||
{
|
||||
|
||||
@@ -20,10 +20,13 @@ namespace Content.Client.GameObjects.Components.HUD.Inventory
|
||||
/// A character UI which shows items the user has equipped within his inventory
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(SharedInventoryComponent))]
|
||||
public class ClientInventoryComponent : SharedInventoryComponent
|
||||
{
|
||||
private readonly Dictionary<Slots, IEntity> _slots = new();
|
||||
|
||||
public IReadOnlyDictionary<Slots, IEntity> AllSlots => _slots;
|
||||
|
||||
[ViewVariables] public InventoryInterfaceController InterfaceController { get; private set; } = default!;
|
||||
|
||||
private ISpriteComponent? _sprite;
|
||||
@@ -70,6 +73,11 @@ namespace Content.Client.GameObjects.Components.HUD.Inventory
|
||||
}
|
||||
}
|
||||
|
||||
public override bool IsEquipped(IEntity item)
|
||||
{
|
||||
return item != null && _slots.Values.Any(e => e == item);
|
||||
}
|
||||
|
||||
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
|
||||
{
|
||||
base.HandleComponentState(curState, nextState);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Client.UserInterface;
|
||||
using Content.Client.Utility;
|
||||
using JetBrains.Annotations;
|
||||
@@ -84,6 +85,16 @@ namespace Content.Client.GameObjects.Components.HUD.Inventory
|
||||
public override SS14Window Window => _window;
|
||||
private HumanInventoryWindow _window;
|
||||
|
||||
public override IEnumerable<ItemSlotButton> GetItemSlotButtons(Slots slot)
|
||||
{
|
||||
if (!_inventoryButtons.TryGetValue(slot, out var buttons))
|
||||
{
|
||||
return Enumerable.Empty<ItemSlotButton>();
|
||||
}
|
||||
|
||||
return buttons;
|
||||
}
|
||||
|
||||
public override void AddToSlot(Slots slot, IEntity entity)
|
||||
{
|
||||
base.AddToSlot(slot, entity);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Content.Client.UserInterface;
|
||||
using Content.Shared.GameObjects.Components.Inventory;
|
||||
using Content.Shared.Input;
|
||||
@@ -53,6 +54,10 @@ namespace Content.Client.GameObjects.Components.HUD.Inventory
|
||||
{
|
||||
}
|
||||
|
||||
/// <returns>the button controls associated with the
|
||||
/// specified slot, if any. Empty if none.</returns>
|
||||
public abstract IEnumerable<ItemSlotButton> GetItemSlotButtons(EquipmentSlotDefines.Slots slot);
|
||||
|
||||
public virtual void AddToSlot(EquipmentSlotDefines.Slots slot, IEntity entity)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace Content.Client.GameObjects.Components.Items
|
||||
{
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(ISharedHandsComponent))]
|
||||
[ComponentReference(typeof(SharedHandsComponent))]
|
||||
public class HandsComponent : SharedHandsComponent
|
||||
{
|
||||
[Dependency] private readonly IGameHud _gameHud = default!;
|
||||
@@ -31,6 +32,18 @@ namespace Content.Client.GameObjects.Components.Items
|
||||
|
||||
[ViewVariables] public IEntity? ActiveHand => GetEntity(ActiveIndex);
|
||||
|
||||
public override bool IsHolding(IEntity entity)
|
||||
{
|
||||
foreach (var hand in _hands)
|
||||
{
|
||||
if (hand.Entity == entity)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void AddHand(Hand hand)
|
||||
{
|
||||
_sprite?.LayerMapReserveBlank($"hand-{hand.Name}");
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
using System;
|
||||
using Content.Shared.Actions;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Client.GameObjects.Components.Mobs.Actions
|
||||
{
|
||||
public struct ActionAssignment : IEquatable<ActionAssignment>
|
||||
{
|
||||
private readonly ActionType _actionType;
|
||||
private readonly ItemActionType _itemActionType;
|
||||
private readonly EntityUid _item;
|
||||
public Assignment Assignment { get; private init; }
|
||||
|
||||
private ActionAssignment(Assignment assignment, ActionType actionType, ItemActionType itemActionType, EntityUid item)
|
||||
{
|
||||
Assignment = assignment;
|
||||
_actionType = actionType;
|
||||
_itemActionType = itemActionType;
|
||||
_item = item;
|
||||
}
|
||||
|
||||
/// <param name="actionType">the action type, if our Assignment is Assignment.Action</param>
|
||||
/// <returns>true only if our Assignment is Assignment.Action</returns>
|
||||
public bool TryGetAction(out ActionType actionType)
|
||||
{
|
||||
actionType = _actionType;
|
||||
return Assignment == Assignment.Action;
|
||||
}
|
||||
|
||||
/// <param name="itemActionType">the item action type, if our Assignment is Assignment.ItemActionWithoutItem</param>
|
||||
/// <returns>true only if our Assignment is Assignment.ItemActionWithoutItem</returns>
|
||||
public bool TryGetItemActionWithoutItem(out ItemActionType itemActionType)
|
||||
{
|
||||
itemActionType = _itemActionType;
|
||||
return Assignment == Assignment.ItemActionWithoutItem;
|
||||
}
|
||||
|
||||
/// <param name="itemActionType">the item action type, if our Assignment is Assignment.ItemActionWithItem</param>
|
||||
/// <param name="item">the item UID providing the action, if our Assignment is Assignment.ItemActionWithItem</param>
|
||||
/// <returns>true only if our Assignment is Assignment.ItemActionWithItem</returns>
|
||||
public bool TryGetItemActionWithItem(out ItemActionType itemActionType, out EntityUid item)
|
||||
{
|
||||
itemActionType = _itemActionType;
|
||||
item = _item;
|
||||
return Assignment == Assignment.ItemActionWithItem;
|
||||
}
|
||||
|
||||
public static ActionAssignment For(ActionType actionType)
|
||||
{
|
||||
return new(Assignment.Action, actionType, default, default);
|
||||
}
|
||||
|
||||
public static ActionAssignment For(ItemActionType actionType)
|
||||
{
|
||||
return new(Assignment.ItemActionWithoutItem, default, actionType, default);
|
||||
}
|
||||
|
||||
public static ActionAssignment For(ItemActionType actionType, EntityUid item)
|
||||
{
|
||||
return new(Assignment.ItemActionWithItem, default, actionType, item);
|
||||
}
|
||||
|
||||
public bool Equals(ActionAssignment other)
|
||||
{
|
||||
return _actionType == other._actionType && _itemActionType == other._itemActionType && Equals(_item, other._item);
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return obj is ActionAssignment other && Equals(other);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(_actionType, _itemActionType, _item);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{nameof(_actionType)}: {_actionType}, {nameof(_itemActionType)}: {_itemActionType}, {nameof(_item)}: {_item}, {nameof(Assignment)}: {Assignment}";
|
||||
}
|
||||
}
|
||||
|
||||
public enum Assignment : byte
|
||||
{
|
||||
Action,
|
||||
ItemActionWithoutItem,
|
||||
ItemActionWithItem
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.GameObjects.Components.Mobs;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Client.GameObjects.Components.Mobs.Actions
|
||||
{
|
||||
/// <summary>
|
||||
/// Tracks and manages the hotbar assignments for actions.
|
||||
/// </summary>
|
||||
public class ActionAssignments
|
||||
{
|
||||
// the slots and assignments fields hold client's assignments (what action goes in what slot),
|
||||
// which are completely client side and independent of what actions they've actually been granted and
|
||||
// what item the action is actually for.
|
||||
|
||||
/// <summary>
|
||||
/// x = hotbar number, y = slot of that hotbar (index 0 corresponds to the one labeled "1",
|
||||
/// index 9 corresponds to the one labeled "0"). Essentially the inverse of _assignments.
|
||||
/// </summary>
|
||||
private readonly ActionAssignment?[,] _slots;
|
||||
|
||||
/// <summary>
|
||||
/// Hotbar and slot assignment for each action type (slot index 0 corresponds to the one labeled "1",
|
||||
/// slot index 9 corresponds to the one labeled "0"). The key corresponds to an index in the _slots array.
|
||||
/// The value is a list because actions can be assigned to multiple slots. Even if an action type has not been granted,
|
||||
/// it can still be assigned to a slot. Essentially the inverse of _slots.
|
||||
/// There will be no entry if there is no assignment (no empty lists in this dict)
|
||||
/// </summary>
|
||||
private readonly Dictionary<ActionAssignment, List<(byte Hotbar, byte Slot)>> _assignments;
|
||||
|
||||
/// <summary>
|
||||
/// Actions which have been manually cleared by the user, thus should not
|
||||
/// auto-populate.
|
||||
/// </summary>
|
||||
private readonly HashSet<ActionType> _preventAutoPopulate = new();
|
||||
private readonly Dictionary<EntityUid, HashSet<ItemActionType>> _preventAutoPopulateItem = new();
|
||||
|
||||
private readonly byte _numHotbars;
|
||||
private readonly byte _numSlots;
|
||||
|
||||
public ActionAssignments(byte numHotbars, byte numSlots)
|
||||
{
|
||||
_numHotbars = numHotbars;
|
||||
_numSlots = numSlots;
|
||||
_assignments = new Dictionary<ActionAssignment, List<(byte Hotbar, byte Slot)>>();
|
||||
_slots = new ActionAssignment?[numHotbars,numSlots];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the assignments based on the current states of all the actions.
|
||||
/// Newly-granted actions or item actions which don't have an assignment will be assigned a slot
|
||||
/// automatically (unless they've been manually cleared). Item-based actions
|
||||
/// which no longer have an associated state will be decoupled from their item.
|
||||
/// </summary>
|
||||
public void Reconcile(byte currentHotbar, IReadOnlyDictionary<ActionType, ActionState> actionStates,
|
||||
IReadOnlyDictionary<EntityUid,Dictionary<ItemActionType, ActionState>> itemActionStates)
|
||||
{
|
||||
// if we've been granted any actions which have no assignment to any hotbar, we must auto-populate them
|
||||
// into the hotbar so the user knows about them.
|
||||
// We fill their current hotbar first, rolling over to the next open slot on the next hotbar.
|
||||
foreach (var actionState in actionStates)
|
||||
{
|
||||
var assignment = ActionAssignment.For(actionState.Key);
|
||||
if (actionState.Value.Enabled && !_assignments.ContainsKey(assignment))
|
||||
{
|
||||
AutoPopulate(assignment, currentHotbar, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
foreach (var (item, itemStates) in itemActionStates)
|
||||
{
|
||||
foreach (var itemActionState in itemStates)
|
||||
{
|
||||
// unlike regular actions, we DO actually show user their new item action even when it's disabled.
|
||||
// this allows them to instantly see when an action may be possible that is provided by an item but
|
||||
// something is preventing it
|
||||
// Note that we are checking if there is an explicit assignment for this item action + item,
|
||||
// we will determine during auto-population if we should tie the item to an existing "item action only"
|
||||
// assignment
|
||||
var assignment = ActionAssignment.For(itemActionState.Key, item);
|
||||
if (!_assignments.ContainsKey(assignment))
|
||||
{
|
||||
AutoPopulate(assignment, currentHotbar, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We need to figure out which current item action assignments we had
|
||||
// which once had an associated item but have been revoked (based on our newly provided action states)
|
||||
// so we can dissociate them from the item. If the provided action states do not
|
||||
// have a state for this action type + item, we can assume that the action has been revoked for that item.
|
||||
var assignmentsWithoutItem = new List<KeyValuePair<ActionAssignment,List<(byte Hotbar, byte Slot)>>>();
|
||||
foreach (var assignmentEntry in _assignments)
|
||||
{
|
||||
if (!assignmentEntry.Key.TryGetItemActionWithItem(out var actionType, out var item)) continue;
|
||||
|
||||
// we have this assignment currently tied to an item,
|
||||
// check if it no longer has an associated item in our dict of states
|
||||
if (itemActionStates.TryGetValue(item, out var states))
|
||||
{
|
||||
if (states.ContainsKey(actionType))
|
||||
{
|
||||
// we have a state for this item + action type so we won't
|
||||
// remove the item from the assignment
|
||||
continue;
|
||||
}
|
||||
}
|
||||
assignmentsWithoutItem.Add(assignmentEntry);
|
||||
}
|
||||
// reassign without the item for each assignment we found that no longer has an associated item
|
||||
foreach (var (assignment, slots) in assignmentsWithoutItem)
|
||||
{
|
||||
foreach (var (hotbar, slot) in slots)
|
||||
{
|
||||
if (!assignment.TryGetItemActionWithItem(out var actionType, out _)) continue;
|
||||
AssignSlot(hotbar, slot,
|
||||
ActionAssignment.For(actionType));
|
||||
}
|
||||
}
|
||||
|
||||
// Additionally, we must find items which have no action states at all in our newly provided states so
|
||||
// we can assume their item was unequipped and reset them to allow auto-population.
|
||||
var itemsWithoutState = _preventAutoPopulateItem.Keys.Where(item => !itemActionStates.ContainsKey(item));
|
||||
foreach (var toRemove in itemsWithoutState)
|
||||
{
|
||||
_preventAutoPopulateItem.Remove(toRemove);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Assigns the indicated hotbar slot to the specified action type.
|
||||
/// </summary>
|
||||
/// <param name="hotbar">hotbar whose slot is being assigned</param>
|
||||
/// <param name="slot">slot of the hotbar to assign to (0 = the slot labeled 1, 9 = the slot labeled 0)</param>
|
||||
/// <param name="actionType">action to assign to the slot</param>
|
||||
public void AssignSlot(byte hotbar, byte slot, ActionAssignment actionType)
|
||||
{
|
||||
ClearSlot(hotbar, slot, false);
|
||||
_slots[hotbar, slot] = actionType;
|
||||
if (_assignments.TryGetValue(actionType, out var slotList))
|
||||
{
|
||||
slotList.Add((hotbar, slot));
|
||||
}
|
||||
else
|
||||
{
|
||||
var newList = new List<(byte Hotbar, byte Slot)> {(hotbar, slot)};
|
||||
_assignments[actionType] = newList;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clear the assignment from the indicated slot.
|
||||
/// </summary>
|
||||
/// <param name="hotbar">hotbar whose slot is being cleared</param>
|
||||
/// <param name="slot">slot of the hotbar to clear (0 = the slot labeled 1, 9 = the slot labeled 0)</param>
|
||||
/// <param name="preventAutoPopulate">if true, the action assigned to this slot
|
||||
/// will be prevented from being auto-populated in the future when it is newly granted.
|
||||
/// Item actions will automatically be allowed to auto populate again
|
||||
/// when their associated item are unequipped. This ensures that items that are newly
|
||||
/// picked up will always present their actions to the user even if they had earlier been cleared.
|
||||
/// </param>
|
||||
public void ClearSlot(byte hotbar, byte slot, bool preventAutoPopulate)
|
||||
{
|
||||
// remove this particular assignment from our data structures
|
||||
// (keeping in mind something can be assigned multiple slots)
|
||||
var currentAction = _slots[hotbar, slot];
|
||||
if (!currentAction.HasValue) return;
|
||||
if (preventAutoPopulate)
|
||||
{
|
||||
var assignment = currentAction.Value;
|
||||
|
||||
if (assignment.TryGetAction(out var actionType))
|
||||
{
|
||||
_preventAutoPopulate.Add(actionType);
|
||||
}
|
||||
else if (assignment.TryGetItemActionWithItem(out var itemActionType, out var item))
|
||||
{
|
||||
if (!_preventAutoPopulateItem.TryGetValue(item, out var actionTypes))
|
||||
{
|
||||
actionTypes = new HashSet<ItemActionType>();
|
||||
_preventAutoPopulateItem[item] = actionTypes;
|
||||
}
|
||||
|
||||
actionTypes.Add(itemActionType);
|
||||
}
|
||||
}
|
||||
var assignmentList = _assignments[currentAction.Value];
|
||||
assignmentList = assignmentList.Where(a => a.Hotbar != hotbar || a.Slot != slot).ToList();
|
||||
if (assignmentList.Count == 0)
|
||||
{
|
||||
_assignments.Remove(currentAction.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
_assignments[currentAction.Value] = assignmentList;
|
||||
}
|
||||
_slots[hotbar, slot] = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the next open slot the action can go in and assigns it there,
|
||||
/// starting from the currently selected hotbar.
|
||||
/// Does not update any UI elements, only updates the assignment data structures.
|
||||
/// </summary>
|
||||
/// <param name="force">if true, will force the assignment to occur
|
||||
/// regardless of whether this assignment has been prevented from auto population
|
||||
/// via ClearSlot's preventAutoPopulate parameter. If false, will have no effect
|
||||
/// if this assignment has been prevented from auto population.</param>
|
||||
public void AutoPopulate(ActionAssignment toAssign, byte currentHotbar, bool force = true)
|
||||
{
|
||||
if (ShouldPreventAutoPopulate(toAssign, force)) return;
|
||||
// if the assignment to make is an item action with an associated item,
|
||||
// then first look for currently assigned item actions without an item, to replace with this
|
||||
// assignment
|
||||
if (toAssign.TryGetItemActionWithItem(out var actionType, out var _))
|
||||
{
|
||||
if (_assignments.TryGetValue(ActionAssignment.For(actionType),
|
||||
out var possibilities))
|
||||
{
|
||||
// use the closest assignment to current hotbar
|
||||
byte hotbar = 0;
|
||||
byte slot = 0;
|
||||
var minCost = int.MaxValue;
|
||||
foreach (var possibility in possibilities)
|
||||
{
|
||||
var cost = possibility.Slot + _numSlots * (currentHotbar >= possibility.Hotbar
|
||||
? currentHotbar - possibility.Hotbar
|
||||
: (_numHotbars - currentHotbar) + possibility.Hotbar);
|
||||
if (cost < minCost)
|
||||
{
|
||||
hotbar = possibility.Hotbar;
|
||||
slot = possibility.Slot;
|
||||
minCost = cost;
|
||||
}
|
||||
}
|
||||
|
||||
if (minCost != int.MaxValue)
|
||||
{
|
||||
AssignSlot(hotbar, slot, toAssign);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (byte hotbarOffset = 0; hotbarOffset < _numHotbars; hotbarOffset++)
|
||||
{
|
||||
for (byte slot = 0; slot < _numSlots; slot++)
|
||||
{
|
||||
var hotbar = (byte) ((currentHotbar + hotbarOffset) % _numHotbars);
|
||||
var slotAssignment = _slots[hotbar, slot];
|
||||
if (slotAssignment.HasValue)
|
||||
{
|
||||
// if the assignment in this slot is an item action without an associated item,
|
||||
// then tie it to the current item if we are trying to auto populate an item action.
|
||||
if (toAssign.Assignment == Assignment.ItemActionWithItem &&
|
||||
slotAssignment.Value.Assignment == Assignment.ItemActionWithoutItem)
|
||||
{
|
||||
AssignSlot(hotbar, slot, toAssign);
|
||||
return;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// slot's empty, assign
|
||||
AssignSlot(hotbar, slot, toAssign);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// there was no empty slot
|
||||
}
|
||||
|
||||
private bool ShouldPreventAutoPopulate(ActionAssignment assignment, bool force)
|
||||
{
|
||||
if (force) return false;
|
||||
|
||||
if (assignment.TryGetAction(out var actionType))
|
||||
{
|
||||
return _preventAutoPopulate.Contains(actionType);
|
||||
}
|
||||
|
||||
if (assignment.TryGetItemActionWithItem(out var itemActionType, out var item))
|
||||
{
|
||||
return _preventAutoPopulateItem.TryGetValue(item,
|
||||
out var itemActionTypes) && itemActionTypes.Contains(itemActionType);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the assignment to the indicated slot if there is one.
|
||||
/// </summary>
|
||||
public ActionAssignment? this[in byte hotbar, in byte slot] => _slots[hotbar, slot];
|
||||
|
||||
/// <returns>true if we have the assignment assigned to some slot</returns>
|
||||
public bool HasAssignment(ActionAssignment assignment)
|
||||
{
|
||||
return _assignments.ContainsKey(assignment);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using Content.Client.GameObjects.Components.HUD.Inventory;
|
||||
using Content.Client.GameObjects.Components.Items;
|
||||
using Content.Client.GameObjects.Components.Mobs.Actions;
|
||||
using Content.Client.UserInterface;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.GameObjects.Components.Mobs;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.GameObjects.EntitySystems;
|
||||
using Robust.Client.Interfaces.UserInterface;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameObjects.ComponentDependencies;
|
||||
using Robust.Shared.GameObjects.Systems;
|
||||
using Robust.Shared.Input.Binding;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Client.GameObjects.Components.Mobs
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(SharedActionsComponent))]
|
||||
public sealed class ClientActionsComponent : SharedActionsComponent
|
||||
{
|
||||
public const byte Hotbars = 10;
|
||||
public const byte Slots = 10;
|
||||
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
|
||||
[ComponentDependency] private readonly HandsComponent? _handsComponent = null;
|
||||
[ComponentDependency] private readonly ClientInventoryComponent? _inventoryComponent = null;
|
||||
|
||||
private ActionsUI? _ui;
|
||||
private readonly List<ItemSlotButton> _highlightingItemSlots = new();
|
||||
|
||||
/// <summary>
|
||||
/// Current assignments for all hotbars / slots for this entity.
|
||||
/// </summary>
|
||||
public ActionAssignments Assignments { get; } = new(Hotbars, Slots);
|
||||
|
||||
/// <summary>
|
||||
/// Allows calculating if we need to act due to this component being controlled by the current mob
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
private bool CurrentlyControlled => _playerManager.LocalPlayer != null && _playerManager.LocalPlayer.ControlledEntity == Owner;
|
||||
|
||||
|
||||
protected override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
PlayerDetached();
|
||||
}
|
||||
|
||||
public override void HandleMessage(ComponentMessage message, IComponent? component)
|
||||
{
|
||||
base.HandleMessage(message, component);
|
||||
switch (message)
|
||||
{
|
||||
case PlayerAttachedMsg _:
|
||||
PlayerAttached();
|
||||
break;
|
||||
case PlayerDetachedMsg _:
|
||||
PlayerDetached();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
|
||||
{
|
||||
base.HandleComponentState(curState, nextState);
|
||||
|
||||
if (curState is not ActionComponentState)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateUI();
|
||||
}
|
||||
|
||||
private void PlayerAttached()
|
||||
{
|
||||
if (!CurrentlyControlled || _ui != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_ui = new ActionsUI(this);
|
||||
IoCManager.Resolve<IUserInterfaceManager>().StateRoot.AddChild(_ui);
|
||||
UpdateUI();
|
||||
}
|
||||
|
||||
private void PlayerDetached()
|
||||
{
|
||||
if (_ui == null) return;
|
||||
IoCManager.Resolve<IUserInterfaceManager>().StateRoot.RemoveChild(_ui);
|
||||
_ui = null;
|
||||
}
|
||||
|
||||
public void HandleHotbarKeybind(byte slot, in PointerInputCmdHandler.PointerInputCmdArgs args)
|
||||
{
|
||||
_ui?.HandleHotbarKeybind(slot, args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the displayed hotbar (and menu) based on current state of actions.
|
||||
/// </summary>
|
||||
private void UpdateUI()
|
||||
{
|
||||
if (!CurrentlyControlled || _ui == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Assignments.Reconcile(_ui.SelectedHotbar, ActionStates(), ItemActionStates());
|
||||
|
||||
_ui.UpdateUI();
|
||||
}
|
||||
|
||||
public void AttemptAction(ActionSlot slot)
|
||||
{
|
||||
|
||||
var attempt = slot.ActionAttempt();
|
||||
if (attempt == null) return;
|
||||
|
||||
switch (attempt.Action.BehaviorType)
|
||||
{
|
||||
case BehaviorType.Instant:
|
||||
// for instant actions, we immediately tell the server we're doing it
|
||||
SendNetworkMessage(attempt.PerformInstantActionMessage());
|
||||
break;
|
||||
case BehaviorType.Toggle:
|
||||
// for toggle actions, we immediately tell the server we're toggling it.
|
||||
if (attempt.TryGetActionState(this, out var actionState))
|
||||
{
|
||||
// TODO: At the moment we always predict that the toggle will work clientside,
|
||||
// even if it sometimes may not (it will be reset by the server if wrong).
|
||||
attempt.ToggleAction(this, !actionState.ToggledOn);
|
||||
slot.ToggledOn = !actionState.ToggledOn;
|
||||
SendNetworkMessage(attempt.PerformToggleActionMessage(!actionState.ToggledOn));
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.ErrorS("action", "attempted to toggle action {0} which has" +
|
||||
" unknown state", attempt);
|
||||
}
|
||||
|
||||
break;
|
||||
case BehaviorType.TargetPoint:
|
||||
case BehaviorType.TargetEntity:
|
||||
// for target actions, we go into "select target" mode, we don't
|
||||
// message the server until we actually pick our target.
|
||||
|
||||
// if we're clicking the same thing we're already targeting for, then we simply cancel
|
||||
// targeting
|
||||
_ui?.ToggleTargeting(slot);
|
||||
break;
|
||||
case BehaviorType.None:
|
||||
break;
|
||||
default:
|
||||
Logger.ErrorS("action", "unhandled action press for action {0}",
|
||||
attempt);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles clicks when selecting the target for an action. Only has an effect when currently
|
||||
/// selecting a target.
|
||||
/// </summary>
|
||||
public bool TargetingOnUse(in PointerInputCmdHandler.PointerInputCmdArgs args)
|
||||
{
|
||||
// not currently predicted
|
||||
if (EntitySystem.Get<InputSystem>().Predicted) return false;
|
||||
|
||||
// only do something for actual target-based actions
|
||||
if (_ui?.SelectingTargetFor?.Action == null ||
|
||||
(_ui.SelectingTargetFor.Action.BehaviorType != BehaviorType.TargetEntity &&
|
||||
_ui.SelectingTargetFor.Action.BehaviorType != BehaviorType.TargetPoint)) return false;
|
||||
|
||||
var attempt = _ui.SelectingTargetFor.ActionAttempt();
|
||||
if (attempt == null)
|
||||
{
|
||||
_ui.StopTargeting();
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (_ui.SelectingTargetFor.Action.BehaviorType)
|
||||
{
|
||||
case BehaviorType.TargetPoint:
|
||||
{
|
||||
// send our action to the server, we chose our target
|
||||
SendNetworkMessage(attempt.PerformTargetPointActionMessage(args));
|
||||
if (!attempt.Action.Repeat)
|
||||
{
|
||||
_ui.StopTargeting();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// target the currently hovered entity, if there is one
|
||||
case BehaviorType.TargetEntity when args.EntityUid != EntityUid.Invalid:
|
||||
{
|
||||
// send our action to the server, we chose our target
|
||||
SendNetworkMessage(attempt.PerformTargetEntityActionMessage(args));
|
||||
if (!attempt.Action.Repeat)
|
||||
{
|
||||
_ui.StopTargeting();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
_ui.StopTargeting();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void AfterActionChanged()
|
||||
{
|
||||
UpdateUI();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Highlights the item slot (inventory or hand) that contains this item
|
||||
/// </summary>
|
||||
/// <param name="item"></param>
|
||||
public void HighlightItemSlot(IEntity item)
|
||||
{
|
||||
StopHighlightingItemSlots();
|
||||
|
||||
// figure out if it's in hand or inventory and highlight it
|
||||
foreach (var hand in _handsComponent!.Hands)
|
||||
{
|
||||
if (hand.Entity != item || hand.Button == null) continue;
|
||||
_highlightingItemSlots.Add(hand.Button);
|
||||
hand.Button.Highlight(true);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var (slot, slotItem) in _inventoryComponent!.AllSlots)
|
||||
{
|
||||
if (slotItem != item) continue;
|
||||
foreach (var itemSlotButton in
|
||||
_inventoryComponent.InterfaceController.GetItemSlotButtons(slot))
|
||||
{
|
||||
_highlightingItemSlots.Add(itemSlotButton);
|
||||
itemSlotButton.Highlight(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops highlighting any item slots we are currently highlighting.
|
||||
/// </summary>
|
||||
public void StopHighlightingItemSlots()
|
||||
{
|
||||
foreach (var itemSlot in _highlightingItemSlots)
|
||||
{
|
||||
itemSlot.Highlight(false);
|
||||
}
|
||||
_highlightingItemSlots.Clear();
|
||||
}
|
||||
|
||||
public void ToggleActionsMenu()
|
||||
{
|
||||
_ui?.ToggleActionsMenu();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,20 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Client.UserInterface;
|
||||
using Content.Client.UserInterface.Stylesheets;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.Alert;
|
||||
using Content.Shared.GameObjects.Components.Mobs;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Interfaces.Graphics;
|
||||
using Robust.Client.Interfaces.ResourceManagement;
|
||||
using Robust.Client.Interfaces.UserInterface;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Interfaces.Timing;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Client.GameObjects.Components.Mobs
|
||||
@@ -29,19 +24,11 @@ namespace Content.Client.GameObjects.Components.Mobs
|
||||
[ComponentReference(typeof(SharedAlertsComponent))]
|
||||
public sealed class ClientAlertsComponent : SharedAlertsComponent
|
||||
{
|
||||
private static readonly float TooltipTextMaxWidth = 265;
|
||||
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IResourceCache _resourceCache = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
|
||||
private AlertsUI _ui;
|
||||
private PanelContainer _tooltip;
|
||||
private RichTextLabel _stateName;
|
||||
private RichTextLabel _stateDescription;
|
||||
private RichTextLabel _stateCooldown;
|
||||
private AlertOrderPrototype _alertOrder;
|
||||
private bool _tooltipReady;
|
||||
|
||||
[ViewVariables]
|
||||
private readonly Dictionary<AlertKey, AlertControl> _alertControls
|
||||
@@ -49,7 +36,6 @@ namespace Content.Client.GameObjects.Components.Mobs
|
||||
|
||||
/// <summary>
|
||||
/// Allows calculating if we need to act due to this component being controlled by the current mob
|
||||
/// TODO: should be revisited after space-wizards/RobustToolbox#1255
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
private bool CurrentlyControlled => _playerManager.LocalPlayer != null && _playerManager.LocalPlayer.ControlledEntity == Owner;
|
||||
@@ -78,14 +64,11 @@ namespace Content.Client.GameObjects.Components.Mobs
|
||||
{
|
||||
base.HandleComponentState(curState, nextState);
|
||||
|
||||
if (curState is not AlertsComponentState state)
|
||||
if (curState is not AlertsComponentState)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// update the dict of states based on the array we got in the message
|
||||
SetAlerts(state.Alerts);
|
||||
|
||||
UpdateAlertsControls();
|
||||
}
|
||||
|
||||
@@ -102,48 +85,24 @@ namespace Content.Client.GameObjects.Components.Mobs
|
||||
Logger.ErrorS("alert", "no alertOrder prototype found, alerts will be in random order");
|
||||
}
|
||||
|
||||
_ui = new AlertsUI(IoCManager.Resolve<IClyde>());
|
||||
var uiManager = IoCManager.Resolve<IUserInterfaceManager>();
|
||||
uiManager.StateRoot.AddChild(_ui);
|
||||
|
||||
_tooltip = new PanelContainer
|
||||
{
|
||||
Visible = false,
|
||||
StyleClasses = { StyleNano.StyleClassTooltipPanel }
|
||||
};
|
||||
var tooltipVBox = new VBoxContainer
|
||||
{
|
||||
RectClipContent = true
|
||||
};
|
||||
_tooltip.AddChild(tooltipVBox);
|
||||
_stateName = new RichTextLabel
|
||||
{
|
||||
MaxWidth = TooltipTextMaxWidth,
|
||||
StyleClasses = { StyleNano.StyleClassTooltipAlertTitle }
|
||||
};
|
||||
tooltipVBox.AddChild(_stateName);
|
||||
_stateDescription = new RichTextLabel
|
||||
{
|
||||
MaxWidth = TooltipTextMaxWidth,
|
||||
StyleClasses = { StyleNano.StyleClassTooltipAlertDescription }
|
||||
};
|
||||
tooltipVBox.AddChild(_stateDescription);
|
||||
_stateCooldown = new RichTextLabel
|
||||
{
|
||||
MaxWidth = TooltipTextMaxWidth,
|
||||
StyleClasses = { StyleNano.StyleClassTooltipAlertCooldown }
|
||||
};
|
||||
tooltipVBox.AddChild(_stateCooldown);
|
||||
|
||||
uiManager.PopupRoot.AddChild(_tooltip);
|
||||
_ui = new AlertsUI();
|
||||
IoCManager.Resolve<IUserInterfaceManager>().StateRoot.AddChild(_ui);
|
||||
|
||||
UpdateAlertsControls();
|
||||
}
|
||||
|
||||
private void PlayerDetached()
|
||||
{
|
||||
_ui?.Dispose();
|
||||
_ui = null;
|
||||
foreach (var alertControl in _alertControls.Values)
|
||||
{
|
||||
alertControl.OnPressed -= AlertControlOnPressed;
|
||||
}
|
||||
|
||||
if (_ui != null)
|
||||
{
|
||||
IoCManager.Resolve<IUserInterfaceManager>().StateRoot.RemoveChild(_ui);
|
||||
_ui = null;
|
||||
}
|
||||
_alertControls.Clear();
|
||||
}
|
||||
|
||||
@@ -168,39 +127,49 @@ namespace Content.Client.GameObjects.Components.Mobs
|
||||
toRemove.Add(existingKey);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var alertKeyToRemove in toRemove)
|
||||
{
|
||||
// remove and dispose the control
|
||||
_alertControls.Remove(alertKeyToRemove, out var control);
|
||||
control?.Dispose();
|
||||
if (control == null) return;
|
||||
_ui.Grid.Children.Remove(control);
|
||||
}
|
||||
|
||||
// now we know that alertControls contains alerts that should still exist but
|
||||
// may need to updated,
|
||||
// also there may be some new alerts we need to show.
|
||||
// further, we need to ensure they are ordered w.r.t their configured order
|
||||
foreach (var alertStatus in EnumerateAlertStates())
|
||||
foreach (var (alertKey, alertState) in EnumerateAlertStates())
|
||||
{
|
||||
if (!AlertManager.TryDecode(alertStatus.AlertEncoded, out var newAlert))
|
||||
if (!alertKey.AlertType.HasValue)
|
||||
{
|
||||
Logger.ErrorS("alert", "Unable to decode alert {0}", alertStatus.AlertEncoded);
|
||||
Logger.WarningS("alert", "found alertkey without alerttype," +
|
||||
" alert keys should never be stored without an alerttype set: {0}", alertKey);
|
||||
continue;
|
||||
}
|
||||
var alertType = alertKey.AlertType.Value;
|
||||
if (!AlertManager.TryGet(alertType, out var newAlert))
|
||||
{
|
||||
Logger.ErrorS("alert", "Unrecognized alertType {0}", alertType);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_alertControls.TryGetValue(newAlert.AlertKey, out var existingAlertControl) &&
|
||||
existingAlertControl.Alert.AlertType == newAlert.AlertType)
|
||||
{
|
||||
// id is the same, simply update the existing control severity
|
||||
existingAlertControl.SetSeverity(alertStatus.Severity);
|
||||
// key is the same, simply update the existing control severity / cooldown
|
||||
existingAlertControl.SetSeverity(alertState.Severity);
|
||||
existingAlertControl.Cooldown = alertState.Cooldown;
|
||||
}
|
||||
else
|
||||
{
|
||||
existingAlertControl?.Dispose();
|
||||
if (existingAlertControl != null)
|
||||
{
|
||||
_ui.Grid.Children.Remove(existingAlertControl);
|
||||
}
|
||||
|
||||
// this is a new alert + alert key or just a different alert with the same
|
||||
// key, create the control and add it in the appropriate order
|
||||
var newAlertControl = CreateAlertControl(newAlert, alertStatus);
|
||||
var newAlertControl = CreateAlertControl(newAlert, alertState);
|
||||
if (_alertOrder != null)
|
||||
{
|
||||
var added = false;
|
||||
@@ -233,14 +202,11 @@ namespace Content.Client.GameObjects.Components.Mobs
|
||||
|
||||
private AlertControl CreateAlertControl(AlertPrototype alert, AlertState alertState)
|
||||
{
|
||||
|
||||
var alertControl = new AlertControl(alert, alertState.Severity, _resourceCache);
|
||||
// show custom tooltip for the status control
|
||||
alertControl.OnShowTooltip += AlertOnOnShowTooltip;
|
||||
alertControl.OnHideTooltip += AlertOnOnHideTooltip;
|
||||
|
||||
var alertControl = new AlertControl(alert, alertState.Severity, _resourceCache)
|
||||
{
|
||||
Cooldown = alertState.Cooldown
|
||||
};
|
||||
alertControl.OnPressed += AlertControlOnPressed;
|
||||
|
||||
return alertControl;
|
||||
}
|
||||
|
||||
@@ -249,36 +215,6 @@ namespace Content.Client.GameObjects.Components.Mobs
|
||||
AlertPressed(args, args.Button as AlertControl);
|
||||
}
|
||||
|
||||
private void AlertOnOnHideTooltip(object sender, EventArgs e)
|
||||
{
|
||||
_tooltipReady = false;
|
||||
_tooltip.Visible = false;
|
||||
}
|
||||
|
||||
private void AlertOnOnShowTooltip(object sender, EventArgs e)
|
||||
{
|
||||
var alertControl = (AlertControl) sender;
|
||||
_stateName.SetMessage(alertControl.Alert.Name);
|
||||
_stateDescription.SetMessage(alertControl.Alert.Description);
|
||||
// check for a cooldown
|
||||
if (alertControl.TotalDuration != null && alertControl.TotalDuration > 0)
|
||||
{
|
||||
_stateCooldown.SetMessage(FormattedMessage.FromMarkup("[color=#776a6a]" +
|
||||
alertControl.TotalDuration +
|
||||
" sec cooldown[/color]"));
|
||||
_stateCooldown.Visible = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_stateCooldown.Visible = false;
|
||||
}
|
||||
// TODO: Text display of cooldown
|
||||
Tooltips.PositionTooltip(_tooltip);
|
||||
// if we set it visible here the size of the previous tooltip will flicker for a frame,
|
||||
// so instead we wait until FrameUpdate to make it visible
|
||||
_tooltipReady = true;
|
||||
}
|
||||
|
||||
private void AlertPressed(BaseButton.ButtonEventArgs args, AlertControl alert)
|
||||
{
|
||||
if (args.Event.Function != EngineKeyFunctions.UIClick)
|
||||
@@ -286,57 +222,17 @@ namespace Content.Client.GameObjects.Components.Mobs
|
||||
return;
|
||||
}
|
||||
|
||||
if (AlertManager.TryEncode(alert.Alert, out var encoded))
|
||||
{
|
||||
SendNetworkMessage(new ClickAlertMessage(encoded));
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.ErrorS("alert", "unable to encode alert {0}", alert.Alert.AlertType);
|
||||
}
|
||||
|
||||
SendNetworkMessage(new ClickAlertMessage(alert.Alert.AlertType));
|
||||
}
|
||||
|
||||
public void FrameUpdate(float frameTime)
|
||||
protected override void AfterShowAlert()
|
||||
{
|
||||
if (_tooltipReady)
|
||||
{
|
||||
_tooltipReady = false;
|
||||
_tooltip.Visible = true;
|
||||
}
|
||||
foreach (var (alertKey, alertControl) in _alertControls)
|
||||
{
|
||||
// reconcile all alert controls with their current cooldowns
|
||||
if (TryGetAlertState(alertKey, out var alertState))
|
||||
{
|
||||
alertControl.UpdateCooldown(alertState.Cooldown, _gameTiming.CurTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.WarningS("alert", "coding error - no alert state for alert {0} " +
|
||||
"even though we had an AlertControl for it, this" +
|
||||
" should never happen", alertControl.Alert.AlertType);
|
||||
}
|
||||
|
||||
}
|
||||
UpdateAlertsControls();
|
||||
}
|
||||
|
||||
protected override void AfterClearAlert()
|
||||
{
|
||||
UpdateAlertsControls();
|
||||
}
|
||||
|
||||
public override void OnRemove()
|
||||
{
|
||||
base.OnRemove();
|
||||
|
||||
foreach (var alertControl in _alertControls.Values)
|
||||
{
|
||||
alertControl.OnShowTooltip -= AlertOnOnShowTooltip;
|
||||
alertControl.OnHideTooltip -= AlertOnOnHideTooltip;
|
||||
alertControl.OnPressed -= AlertControlOnPressed;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
91
Content.Client/GameObjects/EntitySystems/ActionsSystem.cs
Normal file
91
Content.Client/GameObjects/EntitySystems/ActionsSystem.cs
Normal file
@@ -0,0 +1,91 @@
|
||||
using Content.Client.GameObjects.Components.Mobs;
|
||||
using Content.Shared.Input;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.GameObjects.Systems;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Input.Binding;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
namespace Content.Client.GameObjects.EntitySystems
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public class ActionsSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
// set up hotkeys for hotbar
|
||||
CommandBinds.Builder
|
||||
.Bind(ContentKeyFunctions.OpenActionsMenu,
|
||||
InputCmdHandler.FromDelegate(_ => ToggleActionsMenu()))
|
||||
.Bind(ContentKeyFunctions.Hotbar1,
|
||||
HandleHotbarKeybind(0))
|
||||
.Bind(ContentKeyFunctions.Hotbar2,
|
||||
HandleHotbarKeybind(1))
|
||||
.Bind(ContentKeyFunctions.Hotbar3,
|
||||
HandleHotbarKeybind(2))
|
||||
.Bind(ContentKeyFunctions.Hotbar4,
|
||||
HandleHotbarKeybind(3))
|
||||
.Bind(ContentKeyFunctions.Hotbar5,
|
||||
HandleHotbarKeybind(4))
|
||||
.Bind(ContentKeyFunctions.Hotbar6,
|
||||
HandleHotbarKeybind(5))
|
||||
.Bind(ContentKeyFunctions.Hotbar7,
|
||||
HandleHotbarKeybind(6))
|
||||
.Bind(ContentKeyFunctions.Hotbar8,
|
||||
HandleHotbarKeybind(7))
|
||||
.Bind(ContentKeyFunctions.Hotbar9,
|
||||
HandleHotbarKeybind(8))
|
||||
.Bind(ContentKeyFunctions.Hotbar0,
|
||||
HandleHotbarKeybind(9))
|
||||
// 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),
|
||||
typeof(ConstructionSystem), typeof(DragDropSystem))
|
||||
.Register<ActionsSystem>();
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
CommandBinds.Unregister<ActionsSystem>();
|
||||
}
|
||||
|
||||
private PointerInputCmdHandler HandleHotbarKeybind(byte slot)
|
||||
{
|
||||
// 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.HandleHotbarKeybind(slot, args);
|
||||
return true;
|
||||
},
|
||||
false);
|
||||
}
|
||||
|
||||
private bool TargetingOnUse(in PointerInputCmdHandler.PointerInputCmdArgs args)
|
||||
{
|
||||
var playerEntity = _playerManager.LocalPlayer.ControlledEntity;
|
||||
if (playerEntity == null ||
|
||||
!playerEntity.TryGetComponent<ClientActionsComponent>( out var actionsComponent)) return false;
|
||||
|
||||
return actionsComponent.TargetingOnUse(args);
|
||||
}
|
||||
|
||||
private void ToggleActionsMenu()
|
||||
{
|
||||
var playerEntity = _playerManager.LocalPlayer.ControlledEntity;
|
||||
if (playerEntity == null ||
|
||||
!playerEntity.TryGetComponent<ClientActionsComponent>( out var actionsComponent)) return;
|
||||
|
||||
actionsComponent.ToggleActionsMenu();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
using Content.Client.GameObjects.Components.Mobs;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.GameObjects.Systems;
|
||||
using Robust.Shared.Interfaces.Timing;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
namespace Content.Client.GameObjects.EntitySystems
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public class AlertsSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
|
||||
public override void FrameUpdate(float frameTime)
|
||||
{
|
||||
base.FrameUpdate(frameTime);
|
||||
|
||||
if (!_gameTiming.IsFirstTimePredicted)
|
||||
return;
|
||||
|
||||
foreach (var clientAlertsComponent in EntityManager.ComponentManager.EntityQuery<ClientAlertsComponent>())
|
||||
{
|
||||
clientAlertsComponent.FrameUpdate(frameTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Client.State;
|
||||
using Content.Client.Utility;
|
||||
using Content.Shared.GameObjects;
|
||||
using Content.Shared.GameObjects.EntitySystemMessages;
|
||||
using Content.Shared.GameObjects.EntitySystems;
|
||||
@@ -10,7 +11,6 @@ using Robust.Client.GameObjects;
|
||||
using Robust.Client.GameObjects.EntitySystems;
|
||||
using Robust.Client.Graphics.Shaders;
|
||||
using Robust.Client.Interfaces.Graphics.ClientEye;
|
||||
using Robust.Client.Interfaces.Input;
|
||||
using Robust.Client.Interfaces.State;
|
||||
using Robust.Shared.GameObjects.Systems;
|
||||
using Robust.Shared.Input;
|
||||
@@ -18,7 +18,6 @@ using Robust.Shared.Input.Binding;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.GameObjects.EntitySystems
|
||||
@@ -30,12 +29,9 @@ namespace Content.Client.GameObjects.EntitySystems
|
||||
public class DragDropSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IStateManager _stateManager = default!;
|
||||
[Dependency] private readonly IInputManager _inputManager = default!;
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
|
||||
// drag will be triggered when mouse leaves this deadzone around the click position.
|
||||
private const float DragDeadzone = 2f;
|
||||
// how often to recheck possible targets (prevents calling expensive
|
||||
// check logic each update)
|
||||
private const float TargetRecheckInterval = 0.25f;
|
||||
@@ -50,14 +46,10 @@ namespace Content.Client.GameObjects.EntitySystems
|
||||
|
||||
// entity performing the drag action
|
||||
private IEntity _dragger;
|
||||
private IEntity _draggedEntity;
|
||||
private readonly List<IDraggable> _draggables = new();
|
||||
private IEntity _dragShadow;
|
||||
private DragState _state;
|
||||
// time since mouse down over the dragged entity
|
||||
private float _mouseDownTime;
|
||||
// screen pos where the mouse down began
|
||||
private Vector2 _mouseDownScreenPos;
|
||||
// how much time since last recheck of all possible targets
|
||||
private float _targetRecheckTime;
|
||||
// reserved initial mousedown event so we can replay it if no drag ends up being performed
|
||||
@@ -66,6 +58,8 @@ namespace Content.Client.GameObjects.EntitySystems
|
||||
// can ignore any events sent to this system
|
||||
private bool _isReplaying;
|
||||
|
||||
private DragDropHelper<IEntity> _dragDropHelper;
|
||||
|
||||
private ShaderInstance _dropTargetInRangeShader;
|
||||
private ShaderInstance _dropTargetOutOfRangeShader;
|
||||
private SharedInteractionSystem _interactionSystem;
|
||||
@@ -73,20 +67,9 @@ namespace Content.Client.GameObjects.EntitySystems
|
||||
|
||||
private readonly List<SpriteComponent> _highlightedSprites = new();
|
||||
|
||||
private enum DragState : byte
|
||||
{
|
||||
NotDragging,
|
||||
// not dragging yet, waiting to see
|
||||
// if they hold for long enough
|
||||
MouseDown,
|
||||
// currently dragging something
|
||||
Dragging,
|
||||
}
|
||||
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
_state = DragState.NotDragging;
|
||||
_dragDropHelper = new DragDropHelper<IEntity>(OnBeginDrag, OnContinueDrag, OnEndDrag);
|
||||
|
||||
_dropTargetInRangeShader = _prototypeManager.Index<ShaderPrototype>(ShaderDropTargetInRange).Instance();
|
||||
_dropTargetOutOfRangeShader = _prototypeManager.Index<ShaderPrototype>(ShaderDropTargetOutOfRange).Instance();
|
||||
@@ -101,7 +84,7 @@ namespace Content.Client.GameObjects.EntitySystems
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
CancelDrag(false, null);
|
||||
_dragDropHelper.EndDrag();
|
||||
CommandBinds.Unregister<DragDropSystem>();
|
||||
base.Shutdown();
|
||||
}
|
||||
@@ -132,7 +115,7 @@ namespace Content.Client.GameObjects.EntitySystems
|
||||
var dragger = args.Session.AttachedEntity;
|
||||
// cancel any current dragging if there is one (shouldn't be because they would've had to have lifted
|
||||
// the mouse, canceling the drag, but just being cautious)
|
||||
CancelDrag(false, null);
|
||||
_dragDropHelper.EndDrag();
|
||||
|
||||
// possibly initiating a drag
|
||||
// check if the clicked entity is draggable
|
||||
@@ -150,96 +133,43 @@ namespace Content.Client.GameObjects.EntitySystems
|
||||
var dragEventArgs = new StartDragDropEventArgs(args.Session.AttachedEntity, entity);
|
||||
if (draggable.CanStartDrag(dragEventArgs))
|
||||
{
|
||||
// wait to initiate a drag
|
||||
_dragger = dragger;
|
||||
_draggedEntity = entity;
|
||||
_draggables.Add(draggable);
|
||||
_mouseDownTime = 0;
|
||||
_state = DragState.MouseDown;
|
||||
_mouseDownScreenPos = _inputManager.MouseScreenPosition;
|
||||
// don't want anything else to process the click,
|
||||
// but we will save the event so we can "re-play" it if this drag does
|
||||
// not turn into an actual drag so the click can be handled normally
|
||||
_savedMouseDown = args;
|
||||
canDrag = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (canDrag)
|
||||
{
|
||||
// wait to initiate a drag
|
||||
_dragDropHelper.MouseDown(entity);
|
||||
_dragger = dragger;
|
||||
_mouseDownTime = 0;
|
||||
// don't want anything else to process the click,
|
||||
// but we will save the event so we can "re-play" it if this drag does
|
||||
// not turn into an actual drag so the click can be handled normally
|
||||
_savedMouseDown = args;
|
||||
}
|
||||
|
||||
return canDrag;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool OnUseMouseUp(in PointerInputCmdHandler.PointerInputCmdArgs args)
|
||||
|
||||
private bool OnBeginDrag()
|
||||
{
|
||||
if (_state == DragState.MouseDown)
|
||||
if (_dragDropHelper.Dragged == null || _dragDropHelper.Dragged.Deleted)
|
||||
{
|
||||
// quick mouseup, definitely treat it as a normal click by
|
||||
// replaying the original
|
||||
CancelDrag(true, args.OriginalMessage);
|
||||
return false;
|
||||
}
|
||||
if (_state != DragState.Dragging) return false;
|
||||
|
||||
// remaining CancelDrag calls will not replay the click because
|
||||
// by this time we've determined the input was actually a drag attempt
|
||||
|
||||
|
||||
// tell the server we are dropping if we are over a valid drop target in range.
|
||||
// We don't use args.EntityUid here because drag interactions generally should
|
||||
// work even if there's something "on top" of the drop target
|
||||
if (!_interactionSystem.InRangeUnobstructed(_dragger,
|
||||
args.Coordinates, ignoreInsideBlocker: true))
|
||||
{
|
||||
CancelDrag(false, null);
|
||||
// something happened to the clicked entity or we moved the mouse off the target so
|
||||
// we shouldn't replay the original click
|
||||
return false;
|
||||
}
|
||||
|
||||
var entities = GameScreenBase.GetEntitiesUnderPosition(_stateManager, args.Coordinates);
|
||||
|
||||
foreach (var entity in entities)
|
||||
if (_dragDropHelper.Dragged.TryGetComponent<SpriteComponent>(out var draggedSprite))
|
||||
{
|
||||
// check if it's able to be dropped on by current dragged entity
|
||||
var dropArgs = new DragDropEventArgs(_dragger, args.Coordinates, _draggedEntity, entity);
|
||||
|
||||
foreach (var draggable in _draggables)
|
||||
{
|
||||
if (!draggable.CanDrop(dropArgs))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// tell the server about the drop attempt
|
||||
RaiseNetworkEvent(new DragDropMessage(args.Coordinates, _draggedEntity.Uid,
|
||||
entity.Uid));
|
||||
|
||||
draggable.Drop(dropArgs);
|
||||
|
||||
CancelDrag(false, null);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
CancelDrag(false, null);
|
||||
return false;
|
||||
}
|
||||
|
||||
private void StartDragging()
|
||||
{
|
||||
// this is checked elsewhere but adding this as a failsafe
|
||||
if (_draggedEntity == null || _draggedEntity.Deleted)
|
||||
{
|
||||
Logger.Error("Programming error. Cannot initiate drag, no dragged entity or entity" +
|
||||
" was deleted.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_draggedEntity.TryGetComponent<SpriteComponent>(out var draggedSprite))
|
||||
{
|
||||
_state = DragState.Dragging;
|
||||
// pop up drag shadow under mouse
|
||||
var mousePos = _eyeManager.ScreenToMap(_inputManager.MouseScreenPosition);
|
||||
var mousePos = _eyeManager.ScreenToMap(_dragDropHelper.MouseScreenPosition);
|
||||
_dragShadow = EntityManager.SpawnEntity("dragshadow", mousePos);
|
||||
var dragSprite = _dragShadow.GetComponent<SpriteComponent>();
|
||||
dragSprite.CopyFrom(draggedSprite);
|
||||
@@ -249,22 +179,132 @@ namespace Content.Client.GameObjects.EntitySystems
|
||||
dragSprite.DrawDepth = (int) DrawDepth.Overlays;
|
||||
if (dragSprite.Directional)
|
||||
{
|
||||
_dragShadow.Transform.WorldRotation = _draggedEntity.Transform.WorldRotation;
|
||||
_dragShadow.Transform.WorldRotation = _dragDropHelper.Dragged.Transform.WorldRotation;
|
||||
}
|
||||
|
||||
HighlightTargets();
|
||||
|
||||
// drag initiated
|
||||
return true;
|
||||
}
|
||||
else
|
||||
|
||||
Logger.Warning("Unable to display drag shadow for {0} because it" +
|
||||
" has no sprite component.", _dragDropHelper.Dragged.Name);
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool OnContinueDrag(float frameTime)
|
||||
{
|
||||
if (_dragDropHelper.Dragged == null || _dragDropHelper.Dragged.Deleted)
|
||||
{
|
||||
Logger.Warning("Unable to display drag shadow for {0} because it" +
|
||||
" has no sprite component.", _draggedEntity.Name);
|
||||
return false;
|
||||
}
|
||||
// still in range of the thing we are dragging?
|
||||
if (!_interactionSystem.InRangeUnobstructed(_dragger, _dragDropHelper.Dragged))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// keep dragged entity under mouse
|
||||
var mousePos = _eyeManager.ScreenToMap(_dragDropHelper.MouseScreenPosition);
|
||||
// TODO: would use MapPosition instead if it had a setter, but it has no setter.
|
||||
// is that intentional, or should we add a setter for Transform.MapPosition?
|
||||
_dragShadow.Transform.WorldPosition = mousePos.Position;
|
||||
|
||||
_targetRecheckTime += frameTime;
|
||||
if (_targetRecheckTime > TargetRecheckInterval)
|
||||
{
|
||||
HighlightTargets();
|
||||
_targetRecheckTime = 0;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnEndDrag()
|
||||
{
|
||||
RemoveHighlights();
|
||||
if (_dragShadow != null)
|
||||
{
|
||||
EntityManager.DeleteEntity(_dragShadow);
|
||||
}
|
||||
|
||||
_dragShadow = null;
|
||||
_draggables.Clear();
|
||||
_dragger = null;
|
||||
_mouseDownTime = 0;
|
||||
_savedMouseDown = null;
|
||||
}
|
||||
|
||||
private bool OnUseMouseUp(in PointerInputCmdHandler.PointerInputCmdArgs args)
|
||||
{
|
||||
if (!_dragDropHelper.IsDragging)
|
||||
{
|
||||
// haven't started the drag yet, quick mouseup, definitely treat it as a normal click by
|
||||
// replaying the original cmd
|
||||
if (_savedMouseDown.HasValue && _mouseDownTime < MaxMouseDownTimeForReplayingClick)
|
||||
{
|
||||
var savedValue = _savedMouseDown.Value;
|
||||
_isReplaying = true;
|
||||
// adjust the timing info based on the current tick so it appears as if it happened now
|
||||
var replayMsg = savedValue.OriginalMessage;
|
||||
var adjustedInputMsg = new FullInputCmdMessage(args.OriginalMessage.Tick, args.OriginalMessage.SubTick,
|
||||
replayMsg.InputFunctionId, replayMsg.State, replayMsg.Coordinates, replayMsg.ScreenCoordinates, replayMsg.Uid);
|
||||
|
||||
_inputSystem.HandleInputCommand(savedValue.Session, EngineKeyFunctions.Use,
|
||||
adjustedInputMsg, true);
|
||||
_isReplaying = false;
|
||||
}
|
||||
_dragDropHelper.EndDrag();
|
||||
return false;
|
||||
}
|
||||
|
||||
// now when ending the drag, we will not replay the click because
|
||||
// by this time we've determined the input was actually a drag attempt
|
||||
|
||||
// tell the server we are dropping if we are over a valid drop target in range.
|
||||
// We don't use args.EntityUid here because drag interactions generally should
|
||||
// work even if there's something "on top" of the drop target
|
||||
if (!_interactionSystem.InRangeUnobstructed(_dragger,
|
||||
args.Coordinates, ignoreInsideBlocker: true))
|
||||
{
|
||||
_dragDropHelper.EndDrag();
|
||||
return false;
|
||||
}
|
||||
|
||||
var entities = GameScreenBase.GetEntitiesUnderPosition(_stateManager, args.Coordinates);
|
||||
|
||||
foreach (var entity in entities)
|
||||
{
|
||||
// check if it's able to be dropped on by current dragged entity
|
||||
var dropArgs = new DragDropEventArgs(_dragger, args.Coordinates, _dragDropHelper.Dragged, entity);
|
||||
|
||||
foreach (var draggable in _draggables)
|
||||
{
|
||||
if (!draggable.CanDrop(dropArgs))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// tell the server about the drop attempt
|
||||
RaiseNetworkEvent(new DragDropMessage(args.Coordinates, _dragDropHelper.Dragged.Uid,
|
||||
entity.Uid));
|
||||
|
||||
draggable.Drop(dropArgs);
|
||||
|
||||
_dragDropHelper.EndDrag();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
_dragDropHelper.EndDrag();
|
||||
return false;
|
||||
}
|
||||
|
||||
private void HighlightTargets()
|
||||
{
|
||||
if (_state != DragState.Dragging || _draggedEntity == null ||
|
||||
_draggedEntity.Deleted || _dragShadow == null || _dragShadow.Deleted)
|
||||
if (_dragDropHelper.Dragged == null ||
|
||||
_dragDropHelper.Dragged.Deleted || _dragShadow == null || _dragShadow.Deleted)
|
||||
{
|
||||
Logger.Warning("Programming error. Can't highlight drag and drop targets, not currently " +
|
||||
"dragging anything or dragged entity / shadow was deleted.");
|
||||
@@ -289,7 +329,7 @@ namespace Content.Client.GameObjects.EntitySystems
|
||||
if (inRangeSprite.Visible == false) continue;
|
||||
|
||||
// check if it's able to be dropped on by current dragged entity
|
||||
var canDropArgs = new CanDropEventArgs(_dragger, _draggedEntity, pvsEntity);
|
||||
var canDropArgs = new CanDropEventArgs(_dragger, _dragDropHelper.Dragged, pvsEntity);
|
||||
var anyValidDraggable = _draggables.Any(draggable => draggable.CanDrop(canDropArgs));
|
||||
|
||||
if (anyValidDraggable)
|
||||
@@ -314,95 +354,10 @@ namespace Content.Client.GameObjects.EntitySystems
|
||||
_highlightedSprites.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels the drag, firing our saved drag event if instructed to do so and
|
||||
/// we are within the threshold for replaying the click
|
||||
/// (essentially reverting the drag attempt and allowing the original click
|
||||
/// to proceed as if no drag was performed)
|
||||
/// </summary>
|
||||
/// <param name="cause">if fireSavedCmd is true, this should be passed with the value of
|
||||
/// the pointer cmd that caused the drag to be cancelled</param>
|
||||
private void CancelDrag(bool fireSavedCmd, FullInputCmdMessage cause)
|
||||
{
|
||||
RemoveHighlights();
|
||||
if (_dragShadow != null)
|
||||
{
|
||||
EntityManager.DeleteEntity(_dragShadow);
|
||||
}
|
||||
|
||||
_dragShadow = null;
|
||||
_draggedEntity = null;
|
||||
_draggables.Clear();
|
||||
_dragger = null;
|
||||
_state = DragState.NotDragging;
|
||||
|
||||
_mouseDownTime = 0;
|
||||
|
||||
if (fireSavedCmd && _savedMouseDown.HasValue && _mouseDownTime < MaxMouseDownTimeForReplayingClick)
|
||||
{
|
||||
var savedValue = _savedMouseDown.Value;
|
||||
_isReplaying = true;
|
||||
// adjust the timing info based on the current tick so it appears as if it happened now
|
||||
var replayMsg = savedValue.OriginalMessage;
|
||||
var adjustedInputMsg = new FullInputCmdMessage(cause.Tick, cause.SubTick, replayMsg.InputFunctionId, replayMsg.State, replayMsg.Coordinates, replayMsg.ScreenCoordinates, replayMsg.Uid);
|
||||
|
||||
_inputSystem.HandleInputCommand(savedValue.Session, EngineKeyFunctions.Use,
|
||||
adjustedInputMsg, true);
|
||||
_isReplaying = false;
|
||||
}
|
||||
|
||||
_savedMouseDown = null;
|
||||
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
if (_state == DragState.MouseDown)
|
||||
{
|
||||
var screenPos = _inputManager.MouseScreenPosition;
|
||||
if (_draggedEntity == null || _draggedEntity.Deleted)
|
||||
{
|
||||
// something happened to the clicked entity or we moved the mouse off the target so
|
||||
// we shouldn't replay the original click
|
||||
CancelDrag(false, null);
|
||||
return;
|
||||
}
|
||||
else if ((_mouseDownScreenPos - screenPos).Length > DragDeadzone)
|
||||
{
|
||||
// initiate actual drag
|
||||
StartDragging();
|
||||
_mouseDownTime = 0;
|
||||
}
|
||||
}
|
||||
else if (_state == DragState.Dragging)
|
||||
{
|
||||
if (_draggedEntity == null || _draggedEntity.Deleted)
|
||||
{
|
||||
CancelDrag(false, null);
|
||||
return;
|
||||
}
|
||||
// still in range of the thing we are dragging?
|
||||
if (!_interactionSystem.InRangeUnobstructed(_dragger, _draggedEntity))
|
||||
{
|
||||
CancelDrag(false, null);
|
||||
return;
|
||||
}
|
||||
|
||||
// keep dragged entity under mouse
|
||||
var mousePos = _eyeManager.ScreenToMap(_inputManager.MouseScreenPosition);
|
||||
// TODO: would use MapPosition instead if it had a setter, but it has no setter.
|
||||
// is that intentional, or should we add a setter for Transform.MapPosition?
|
||||
_dragShadow.Transform.WorldPosition = mousePos.Position;
|
||||
|
||||
_targetRecheckTime += frameTime;
|
||||
if (_targetRecheckTime > TargetRecheckInterval)
|
||||
{
|
||||
HighlightTargets();
|
||||
_targetRecheckTime = 0;
|
||||
}
|
||||
|
||||
}
|
||||
_dragDropHelper.Update(frameTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,6 +210,8 @@
|
||||
"CrematoriumEntityStorage",
|
||||
"RandomArcade",
|
||||
"RandomSpriteState",
|
||||
"DebugEquip",
|
||||
"InnateActions",
|
||||
"ReagentGrinder",
|
||||
"Grindable",
|
||||
"Juiceable",
|
||||
|
||||
@@ -46,6 +46,17 @@ namespace Content.Client.Input
|
||||
human.AddFunction(ContentKeyFunctions.Arcade1);
|
||||
human.AddFunction(ContentKeyFunctions.Arcade2);
|
||||
human.AddFunction(ContentKeyFunctions.Arcade3);
|
||||
human.AddFunction(ContentKeyFunctions.OpenActionsMenu);
|
||||
human.AddFunction(ContentKeyFunctions.Hotbar0);
|
||||
human.AddFunction(ContentKeyFunctions.Hotbar1);
|
||||
human.AddFunction(ContentKeyFunctions.Hotbar2);
|
||||
human.AddFunction(ContentKeyFunctions.Hotbar3);
|
||||
human.AddFunction(ContentKeyFunctions.Hotbar4);
|
||||
human.AddFunction(ContentKeyFunctions.Hotbar5);
|
||||
human.AddFunction(ContentKeyFunctions.Hotbar6);
|
||||
human.AddFunction(ContentKeyFunctions.Hotbar7);
|
||||
human.AddFunction(ContentKeyFunctions.Hotbar8);
|
||||
human.AddFunction(ContentKeyFunctions.Hotbar9);
|
||||
|
||||
var ghost = contexts.New("ghost", "common");
|
||||
ghost.AddFunction(EngineKeyFunctions.MoveUp);
|
||||
|
||||
100
Content.Client/UserInterface/ActionAlertTooltip.cs
Normal file
100
Content.Client/UserInterface/ActionAlertTooltip.cs
Normal file
@@ -0,0 +1,100 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using Content.Client.UserInterface.Stylesheets;
|
||||
using Content.Shared.Actions;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Interfaces.Timing;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.UserInterface
|
||||
{
|
||||
/// <summary>
|
||||
/// Tooltip for actions or alerts because they are very similar.
|
||||
/// </summary>
|
||||
public class ActionAlertTooltip : PanelContainer
|
||||
{
|
||||
private const float TooltipTextMaxWidth = 350;
|
||||
|
||||
private readonly RichTextLabel _cooldownLabel;
|
||||
private readonly IGameTiming _gameTiming;
|
||||
|
||||
/// <summary>
|
||||
/// Current cooldown displayed in this tooltip. Set to null to show no cooldown.
|
||||
/// </summary>
|
||||
public (TimeSpan Start, TimeSpan End)? Cooldown { get; set; }
|
||||
|
||||
public ActionAlertTooltip(FormattedMessage name, FormattedMessage? desc, string? requires = null)
|
||||
{
|
||||
_gameTiming = IoCManager.Resolve<IGameTiming>();
|
||||
|
||||
SetOnlyStyleClass(StyleNano.StyleClassTooltipPanel);
|
||||
|
||||
VBoxContainer vbox;
|
||||
AddChild(vbox = new VBoxContainer {RectClipContent = true});
|
||||
var nameLabel = new RichTextLabel
|
||||
{
|
||||
MaxWidth = TooltipTextMaxWidth,
|
||||
StyleClasses = {StyleNano.StyleClassTooltipActionTitle}
|
||||
};
|
||||
nameLabel.SetMessage(name);
|
||||
vbox.AddChild(nameLabel);
|
||||
|
||||
if (desc != null && !string.IsNullOrWhiteSpace(desc.ToString()))
|
||||
{
|
||||
var description = new RichTextLabel
|
||||
{
|
||||
MaxWidth = TooltipTextMaxWidth,
|
||||
StyleClasses = {StyleNano.StyleClassTooltipActionDescription}
|
||||
};
|
||||
description.SetMessage(desc);
|
||||
vbox.AddChild(description);
|
||||
}
|
||||
|
||||
vbox.AddChild(_cooldownLabel = new RichTextLabel
|
||||
{
|
||||
MaxWidth = TooltipTextMaxWidth,
|
||||
StyleClasses = {StyleNano.StyleClassTooltipActionCooldown},
|
||||
Visible = false
|
||||
});
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(requires))
|
||||
{
|
||||
var requiresLabel = new RichTextLabel
|
||||
{
|
||||
MaxWidth = TooltipTextMaxWidth,
|
||||
StyleClasses = {StyleNano.StyleClassTooltipActionRequirements}
|
||||
};
|
||||
requiresLabel.SetMessage(FormattedMessage.FromMarkup("[color=#635c5c]" +
|
||||
requires +
|
||||
"[/color]"));
|
||||
vbox.AddChild(requiresLabel);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
if (!Cooldown.HasValue)
|
||||
{
|
||||
_cooldownLabel.Visible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var timeLeft = Cooldown.Value.End - _gameTiming.CurTime;
|
||||
if (timeLeft > TimeSpan.Zero)
|
||||
{
|
||||
var duration = Cooldown.Value.End - Cooldown.Value.Start;
|
||||
_cooldownLabel.SetMessage(FormattedMessage.FromMarkup(
|
||||
$"[color=#a10505]{duration.Seconds} sec cooldown ({timeLeft.Seconds + 1} sec remaining)[/color]"));
|
||||
_cooldownLabel.Visible = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_cooldownLabel.Visible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
499
Content.Client/UserInterface/ActionMenu.cs
Normal file
499
Content.Client/UserInterface/ActionMenu.cs
Normal file
@@ -0,0 +1,499 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Content.Client.GameObjects.Components.Mobs;
|
||||
using Content.Client.GameObjects.Components.Mobs.Actions;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Client.UserInterface.Stylesheets;
|
||||
using Content.Client.Utility;
|
||||
using Content.Shared.Actions;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.UserInterface
|
||||
{
|
||||
/// <summary>
|
||||
/// Action selection menu, allows filtering and searching over all possible
|
||||
/// actions and populating those actions into the hotbar.
|
||||
/// </summary>
|
||||
public class ActionMenu : SS14Window
|
||||
{
|
||||
private const string ItemTag = "item";
|
||||
private const string NotItemTag = "not item";
|
||||
private const string InstantActionTag = "instant";
|
||||
private const string ToggleActionTag = "toggle";
|
||||
private const string TargetActionTag = "target";
|
||||
private const string AllActionsTag = "all";
|
||||
private const string GrantedActionsTag = "granted";
|
||||
private const int MinSearchLength = 3;
|
||||
private static readonly Regex NonAlphanumeric = new Regex(@"\W", RegexOptions.Compiled);
|
||||
private static readonly Regex Whitespace = new Regex(@"\s+", RegexOptions.Compiled);
|
||||
private static readonly BaseActionPrototype[] EmptyActionList = Array.Empty<BaseActionPrototype>();
|
||||
|
||||
// parallel list of actions currently selectable in itemList
|
||||
private BaseActionPrototype[] _actionList;
|
||||
|
||||
private readonly ActionManager _actionManager;
|
||||
private readonly ClientActionsComponent _actionsComponent;
|
||||
private readonly ActionsUI _actionsUI;
|
||||
private readonly LineEdit _searchBar;
|
||||
private readonly MultiselectOptionButton<string> _filterButton;
|
||||
private readonly Label _filterLabel;
|
||||
private readonly Button _clearButton;
|
||||
private readonly GridContainer _resultsGrid;
|
||||
private readonly TextureRect _dragShadow;
|
||||
private readonly DragDropHelper<ActionMenuItem> _dragDropHelper;
|
||||
|
||||
|
||||
public ActionMenu(ClientActionsComponent actionsComponent, ActionsUI actionsUI)
|
||||
{
|
||||
_actionsComponent = actionsComponent;
|
||||
_actionsUI = actionsUI;
|
||||
_actionManager = IoCManager.Resolve<ActionManager>();
|
||||
Title = Loc.GetString("Actions");
|
||||
CustomMinimumSize = (300, 300);
|
||||
|
||||
Contents.AddChild(new VBoxContainer
|
||||
{
|
||||
Children =
|
||||
{
|
||||
new HBoxContainer
|
||||
{
|
||||
Children =
|
||||
{
|
||||
(_searchBar = new LineEdit
|
||||
{
|
||||
StyleClasses = { StyleNano.StyleClassActionSearchBox },
|
||||
SizeFlagsHorizontal = SizeFlags.FillExpand,
|
||||
PlaceHolder = Loc.GetString("Search")
|
||||
}),
|
||||
(_filterButton = new MultiselectOptionButton<string>()
|
||||
{
|
||||
Label = Loc.GetString("Filter")
|
||||
}),
|
||||
}
|
||||
},
|
||||
(_clearButton = new Button
|
||||
{
|
||||
Text = Loc.GetString("Clear"),
|
||||
}),
|
||||
(_filterLabel = new Label()),
|
||||
new ScrollContainer
|
||||
{
|
||||
//TODO: needed? CustomMinimumSize = new Vector2(200.0f, 0.0f),
|
||||
SizeFlagsVertical = SizeFlags.FillExpand,
|
||||
SizeFlagsHorizontal = SizeFlags.FillExpand,
|
||||
Children =
|
||||
{
|
||||
(_resultsGrid = new GridContainer
|
||||
{
|
||||
MaxWidth = 300
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// populate filters from search tags
|
||||
var filterTags = new List<string>();
|
||||
foreach (var action in _actionManager.EnumerateActions())
|
||||
{
|
||||
filterTags.AddRange(action.Filters);
|
||||
}
|
||||
|
||||
// special one to filter to only include item actions
|
||||
filterTags.Add(ItemTag);
|
||||
filterTags.Add(NotItemTag);
|
||||
filterTags.Add(InstantActionTag);
|
||||
filterTags.Add(ToggleActionTag);
|
||||
filterTags.Add(TargetActionTag);
|
||||
filterTags.Add(AllActionsTag);
|
||||
filterTags.Add(GrantedActionsTag);
|
||||
|
||||
foreach (var tag in filterTags.Distinct().OrderBy(tag => tag))
|
||||
{
|
||||
_filterButton.AddItem( CultureInfo.CurrentCulture.TextInfo.ToTitleCase(tag), tag);
|
||||
}
|
||||
|
||||
UpdateFilterLabel();
|
||||
|
||||
_dragShadow = new TextureRect
|
||||
{
|
||||
CustomMinimumSize = (64, 64),
|
||||
Stretch = TextureRect.StretchMode.Scale,
|
||||
Visible = false
|
||||
};
|
||||
UserInterfaceManager.PopupRoot.AddChild(_dragShadow);
|
||||
LayoutContainer.SetSize(_dragShadow, (64, 64));
|
||||
|
||||
_dragDropHelper = new DragDropHelper<ActionMenuItem>(OnBeginActionDrag, OnContinueActionDrag, OnEndActionDrag);
|
||||
}
|
||||
|
||||
|
||||
protected override void EnteredTree()
|
||||
{
|
||||
base.EnteredTree();
|
||||
_clearButton.OnPressed += OnClearButtonPressed;
|
||||
_searchBar.OnTextChanged += OnSearchTextChanged;
|
||||
_filterButton.OnItemSelected += OnFilterItemSelected;
|
||||
|
||||
foreach (var actionMenuControl in _resultsGrid.Children)
|
||||
{
|
||||
var actionMenuItem = (actionMenuControl as ActionMenuItem);
|
||||
actionMenuItem.OnButtonDown += OnItemButtonDown;
|
||||
actionMenuItem.OnButtonUp += OnItemButtonUp;
|
||||
actionMenuItem.OnPressed += OnItemPressed;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ExitedTree()
|
||||
{
|
||||
base.ExitedTree();
|
||||
_clearButton.OnPressed -= OnClearButtonPressed;
|
||||
_searchBar.OnTextChanged -= OnSearchTextChanged;
|
||||
_filterButton.OnItemSelected -= OnFilterItemSelected;
|
||||
|
||||
foreach (var actionMenuControl in _resultsGrid.Children)
|
||||
{
|
||||
var actionMenuItem = (actionMenuControl as ActionMenuItem);
|
||||
actionMenuItem.OnButtonDown -= OnItemButtonDown;
|
||||
actionMenuItem.OnButtonUp -= OnItemButtonUp;
|
||||
actionMenuItem.OnPressed -= OnItemPressed;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnFilterItemSelected(MultiselectOptionButton<string>.ItemPressedEventArgs args)
|
||||
{
|
||||
UpdateFilterLabel();
|
||||
SearchAndDisplay();
|
||||
}
|
||||
|
||||
protected override void Resized()
|
||||
{
|
||||
base.Resized();
|
||||
// TODO: Can rework this once https://github.com/space-wizards/RobustToolbox/issues/1392 is done,
|
||||
// currently no good way to let the grid know what size it has to "work with", so must manually resize
|
||||
_resultsGrid.MaxWidth = Width;
|
||||
}
|
||||
|
||||
private bool OnBeginActionDrag()
|
||||
{
|
||||
_dragShadow.Texture = _dragDropHelper.Dragged.Action.Icon.Frame0();
|
||||
// don't make visible until frameupdate, otherwise it'll flicker
|
||||
LayoutContainer.SetPosition(_dragShadow, UserInterfaceManager.MousePositionScaled - (32, 32));
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool OnContinueActionDrag(float frameTime)
|
||||
{
|
||||
// keep dragged entity centered under mouse
|
||||
LayoutContainer.SetPosition(_dragShadow, UserInterfaceManager.MousePositionScaled - (32, 32));
|
||||
// we don't set this visible until frameupdate, otherwise it flickers
|
||||
_dragShadow.Visible = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnEndActionDrag()
|
||||
{
|
||||
_dragShadow.Visible = false;
|
||||
}
|
||||
|
||||
private void OnItemButtonDown(BaseButton.ButtonEventArgs args)
|
||||
{
|
||||
if (args.Event.Function != EngineKeyFunctions.UIClick) return;
|
||||
_dragDropHelper.MouseDown(args.Button as ActionMenuItem);
|
||||
}
|
||||
|
||||
private void OnItemButtonUp(BaseButton.ButtonEventArgs args)
|
||||
{
|
||||
// note the buttonup only fires on the control that was originally
|
||||
// pressed to initiate the drag, NOT the one we are currently hovering
|
||||
if (args.Event.Function != EngineKeyFunctions.UIClick) return;
|
||||
|
||||
if (UserInterfaceManager.CurrentlyHovered is ActionSlot targetSlot)
|
||||
{
|
||||
if (!_dragDropHelper.IsDragging || _dragDropHelper.Dragged?.Action == null)
|
||||
{
|
||||
_dragDropHelper.EndDrag();
|
||||
return;
|
||||
}
|
||||
|
||||
// drag and drop
|
||||
switch (_dragDropHelper.Dragged.Action)
|
||||
{
|
||||
// assign the dragged action to the target slot
|
||||
case ActionPrototype actionPrototype:
|
||||
_actionsComponent.Assignments.AssignSlot(_actionsUI.SelectedHotbar, targetSlot.SlotIndex, ActionAssignment.For(actionPrototype.ActionType));
|
||||
break;
|
||||
case ItemActionPrototype itemActionPrototype:
|
||||
// the action menu doesn't show us if the action has an associated item,
|
||||
// so when we perform the assignment, we should check if we currently have an unassigned state
|
||||
// for this item and assign it tied to that item if so, otherwise assign it "itemless"
|
||||
|
||||
// this is not particularly efficient but we don't maintain an index from
|
||||
// item action type to its action states, and this method should be pretty infrequent so it's probably fine
|
||||
var assigned = false;
|
||||
foreach (var (item, itemStates) in _actionsComponent.ItemActionStates())
|
||||
{
|
||||
foreach (var (actionType, _) in itemStates)
|
||||
{
|
||||
if (actionType != itemActionPrototype.ActionType) continue;
|
||||
var assignment = ActionAssignment.For(actionType, item);
|
||||
if (_actionsComponent.Assignments.HasAssignment(assignment)) continue;
|
||||
// no assignment for this state, assign tied to the item
|
||||
assigned = true;
|
||||
_actionsComponent.Assignments.AssignSlot(_actionsUI.SelectedHotbar, targetSlot.SlotIndex, assignment);
|
||||
break;
|
||||
}
|
||||
|
||||
if (assigned)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!assigned)
|
||||
{
|
||||
_actionsComponent.Assignments.AssignSlot(_actionsUI.SelectedHotbar, targetSlot.SlotIndex, ActionAssignment.For(itemActionPrototype.ActionType));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
_actionsUI.UpdateUI();
|
||||
}
|
||||
|
||||
_dragDropHelper.EndDrag();
|
||||
}
|
||||
|
||||
private void OnItemPressed(BaseButton.ButtonEventArgs args)
|
||||
{
|
||||
if (args.Button is not ActionMenuItem actionMenuItem) return;
|
||||
switch (actionMenuItem.Action)
|
||||
{
|
||||
case ActionPrototype actionPrototype:
|
||||
_actionsComponent.Assignments.AutoPopulate(ActionAssignment.For(actionPrototype.ActionType), _actionsUI.SelectedHotbar);
|
||||
break;
|
||||
case ItemActionPrototype itemActionPrototype:
|
||||
_actionsComponent.Assignments.AutoPopulate(ActionAssignment.For(itemActionPrototype.ActionType), _actionsUI.SelectedHotbar);
|
||||
break;
|
||||
default:
|
||||
Logger.ErrorS("action", "unexpected action prototype {0}", actionMenuItem.Action);
|
||||
break;
|
||||
}
|
||||
|
||||
_actionsUI.UpdateUI();
|
||||
}
|
||||
|
||||
private void OnClearButtonPressed(BaseButton.ButtonEventArgs args)
|
||||
{
|
||||
_searchBar.Clear();
|
||||
_filterButton.DeselectAll();
|
||||
UpdateFilterLabel();
|
||||
SearchAndDisplay();
|
||||
}
|
||||
|
||||
private void OnSearchTextChanged(LineEdit.LineEditEventArgs obj)
|
||||
{
|
||||
SearchAndDisplay();
|
||||
}
|
||||
|
||||
private void SearchAndDisplay()
|
||||
{
|
||||
var search = Standardize(_searchBar.Text);
|
||||
// only display nothing if there are no filters selected and text is not long enough.
|
||||
// otherwise we will search if even one filter is applied, regardless of length of search string
|
||||
if (_filterButton.SelectedKeys.Count == 0 &&
|
||||
(string.IsNullOrWhiteSpace(search) || search.Length < MinSearchLength))
|
||||
{
|
||||
ClearList();
|
||||
return;
|
||||
}
|
||||
|
||||
var matchingActions = _actionManager.EnumerateActions()
|
||||
.Where(a => MatchesSearchCriteria(a, search, _filterButton.SelectedKeys));
|
||||
|
||||
PopulateActions(matchingActions);
|
||||
}
|
||||
|
||||
private void UpdateFilterLabel()
|
||||
{
|
||||
if (_filterButton.SelectedKeys.Count == 0)
|
||||
{
|
||||
_filterLabel.Visible = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
_filterLabel.Visible = true;
|
||||
_filterLabel.Text = Loc.GetString("Filters: {0}", string.Join(", ", _filterButton.SelectedLabels));
|
||||
}
|
||||
}
|
||||
|
||||
private bool MatchesSearchCriteria(BaseActionPrototype action, string standardizedSearch,
|
||||
IReadOnlyList<string> selectedFilterTags)
|
||||
{
|
||||
// check filter tag match first - each action must contain all filter tags currently selected.
|
||||
// if no tags selected, don't check tags
|
||||
if (selectedFilterTags.Count > 0 && selectedFilterTags.Any(filterTag => !ActionMatchesFilterTag(action, filterTag)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// check search tag match against the search query
|
||||
if (action.Keywords.Any(standardizedSearch.Contains))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Standardize(ActionTypeString(action)).Contains(standardizedSearch))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// allows matching by typing spaces between the enum case changes, like "xeno spit" if the
|
||||
// actiontype is "XenoSpit"
|
||||
if (Standardize(ActionTypeString(action), true).Contains(standardizedSearch))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Standardize(action.Name.ToString()).Contains(standardizedSearch))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
private string ActionTypeString(BaseActionPrototype baseActionPrototype)
|
||||
{
|
||||
if (baseActionPrototype is ActionPrototype actionPrototype)
|
||||
{
|
||||
return actionPrototype.ActionType.ToString();
|
||||
}
|
||||
if (baseActionPrototype is ItemActionPrototype itemActionPrototype)
|
||||
{
|
||||
return itemActionPrototype.ActionType.ToString();
|
||||
}
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
private bool ActionMatchesFilterTag(BaseActionPrototype action, string tag)
|
||||
{
|
||||
return tag switch
|
||||
{
|
||||
AllActionsTag => true,
|
||||
GrantedActionsTag => _actionsComponent.IsGranted(action),
|
||||
ItemTag => action is ItemActionPrototype,
|
||||
NotItemTag => action is ActionPrototype,
|
||||
InstantActionTag => action.BehaviorType == BehaviorType.Instant,
|
||||
TargetActionTag => action.BehaviorType == BehaviorType.TargetEntity ||
|
||||
action.BehaviorType == BehaviorType.TargetPoint,
|
||||
ToggleActionTag => action.BehaviorType == BehaviorType.Toggle,
|
||||
_ => action.Filters.Contains(tag)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Standardized form is all lowercase, no non-alphanumeric characters (converted to whitespace),
|
||||
/// trimmed, 1 space max per whitespace gap,
|
||||
/// and optional spaces between case change
|
||||
/// </summary>
|
||||
private static string Standardize(string rawText, bool splitOnCaseChange = false)
|
||||
{
|
||||
rawText ??= "";
|
||||
|
||||
// treat non-alphanumeric characters as whitespace
|
||||
rawText = NonAlphanumeric.Replace(rawText, " ");
|
||||
|
||||
// trim spaces and reduce internal whitespaces to 1 max
|
||||
rawText = Whitespace.Replace(rawText, " ").Trim();
|
||||
if (splitOnCaseChange)
|
||||
{
|
||||
// insert a space when case switches from lower to upper
|
||||
rawText = AddSpaces(rawText, true);
|
||||
}
|
||||
|
||||
return rawText.ToLowerInvariant().Trim();
|
||||
}
|
||||
|
||||
// taken from https://stackoverflow.com/a/272929 (CC BY-SA 3.0)
|
||||
private static string AddSpaces(string text, bool preserveAcronyms)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
return string.Empty;
|
||||
var newText = new StringBuilder(text.Length * 2);
|
||||
newText.Append(text[0]);
|
||||
for (var i = 1; i < text.Length; i++)
|
||||
{
|
||||
if (char.IsUpper(text[i]))
|
||||
{
|
||||
if ((text[i - 1] != ' ' && !char.IsUpper(text[i - 1])) ||
|
||||
(preserveAcronyms && char.IsUpper(text[i - 1]) &&
|
||||
i < text.Length - 1 && !char.IsUpper(text[i + 1])))
|
||||
newText.Append(' ');
|
||||
}
|
||||
|
||||
newText.Append(text[i]);
|
||||
}
|
||||
return newText.ToString();
|
||||
}
|
||||
|
||||
private void PopulateActions(IEnumerable<BaseActionPrototype> actions)
|
||||
{
|
||||
ClearList();
|
||||
|
||||
_actionList = actions.ToArray();
|
||||
foreach (var action in _actionList.OrderBy(act => act.Name.ToString()))
|
||||
{
|
||||
var actionItem = new ActionMenuItem(action);
|
||||
_resultsGrid.Children.Add(actionItem);
|
||||
actionItem.SetActionState(_actionsComponent.IsGranted(action));
|
||||
|
||||
actionItem.OnButtonDown += OnItemButtonDown;
|
||||
actionItem.OnButtonUp += OnItemButtonUp;
|
||||
actionItem.OnPressed += OnItemPressed;
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearList()
|
||||
{
|
||||
// TODO: Not sure if this unsub is needed if children are all being cleared
|
||||
foreach (var actionItem in _resultsGrid.Children)
|
||||
{
|
||||
((ActionMenuItem) actionItem).OnPressed -= OnItemPressed;
|
||||
}
|
||||
_resultsGrid.Children.Clear();
|
||||
_actionList = EmptyActionList;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Should be invoked when action states change, ensures
|
||||
/// currently displayed actions are properly showing their revoked / granted status
|
||||
/// </summary>
|
||||
public void UpdateUI()
|
||||
{
|
||||
foreach (var actionItem in _resultsGrid.Children)
|
||||
{
|
||||
var actionMenuItem = ((ActionMenuItem) actionItem);
|
||||
actionMenuItem.SetActionState(_actionsComponent.IsGranted(actionMenuItem.Action));
|
||||
}
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.Update(args);
|
||||
_dragDropHelper.Update(args.DeltaSeconds);
|
||||
}
|
||||
}
|
||||
}
|
||||
68
Content.Client/UserInterface/ActionMenuItem.cs
Normal file
68
Content.Client/UserInterface/ActionMenuItem.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
#nullable enable
|
||||
|
||||
using Content.Client.UserInterface.Stylesheets;
|
||||
using Content.Shared.Actions;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.Utility;
|
||||
|
||||
namespace Content.Client.UserInterface
|
||||
{
|
||||
/// <summary>
|
||||
/// An individual action visible in the action menu.
|
||||
/// </summary>
|
||||
public class ActionMenuItem : ContainerButton
|
||||
{
|
||||
// shorter than default tooltip delay so user can
|
||||
// quickly explore what each action is
|
||||
private const float CustomTooltipDelay = 0.2f;
|
||||
|
||||
public BaseActionPrototype Action { get; private set; }
|
||||
|
||||
public ActionMenuItem(BaseActionPrototype action)
|
||||
{
|
||||
Action = action;
|
||||
|
||||
CustomMinimumSize = (64, 64);
|
||||
SizeFlagsVertical = SizeFlags.None;
|
||||
|
||||
AddChild(new TextureRect
|
||||
{
|
||||
SizeFlagsHorizontal = SizeFlags.FillExpand,
|
||||
SizeFlagsVertical = SizeFlags.FillExpand,
|
||||
Stretch = TextureRect.StretchMode.Scale,
|
||||
Texture = action.Icon.Frame0()
|
||||
});
|
||||
|
||||
TooltipDelay = CustomTooltipDelay;
|
||||
TooltipSupplier = SupplyTooltip;
|
||||
}
|
||||
|
||||
private Control SupplyTooltip(Control? sender)
|
||||
{
|
||||
return new ActionAlertTooltip(Action.Name, Action.Description, Action.Requires);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Change how this is displayed depending on if it is granted or revoked
|
||||
/// </summary>
|
||||
public void SetActionState(bool granted)
|
||||
{
|
||||
if (granted)
|
||||
{
|
||||
if (HasStyleClass(StyleNano.StyleClassActionMenuItemRevoked))
|
||||
{
|
||||
RemoveStyleClass(StyleNano.StyleClassActionMenuItemRevoked);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!HasStyleClass(StyleNano.StyleClassActionMenuItemRevoked))
|
||||
{
|
||||
AddStyleClass(StyleNano.StyleClassActionMenuItemRevoked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
556
Content.Client/UserInterface/ActionsUI.cs
Normal file
556
Content.Client/UserInterface/ActionsUI.cs
Normal file
@@ -0,0 +1,556 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using Content.Client.GameObjects.Components.Mobs;
|
||||
using Content.Client.GameObjects.Components.Mobs.Actions;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Client.UserInterface.Stylesheets;
|
||||
using Content.Client.Utility;
|
||||
using Content.Shared.Actions;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Interfaces.ResourceManagement;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Input.Binding;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Interfaces.Timing;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.UserInterface
|
||||
{
|
||||
/// <summary>
|
||||
/// The action hotbar on the left side of the screen.
|
||||
/// </summary>
|
||||
public sealed class ActionsUI : Container
|
||||
{
|
||||
private readonly ClientActionsComponent _actionsComponent;
|
||||
private readonly ActionManager _actionManager;
|
||||
private readonly IEntityManager _entityManager;
|
||||
private readonly IGameTiming _gameTiming;
|
||||
|
||||
private readonly ActionSlot[] _slots;
|
||||
|
||||
private readonly GridContainer _slotContainer;
|
||||
|
||||
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 TextureRect _dragShadow;
|
||||
|
||||
private readonly ActionMenu _menu;
|
||||
|
||||
/// <summary>
|
||||
/// Index of currently selected hotbar
|
||||
/// </summary>
|
||||
public byte SelectedHotbar { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Action slot we are currently selecting a target for.
|
||||
/// </summary>
|
||||
public ActionSlot? SelectingTargetFor { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Drag drop helper for coordinating drag drops between action slots
|
||||
/// </summary>
|
||||
public DragDropHelper<ActionSlot> DragDropHelper { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the bar is currently locked by the user. This is intended to prevent drag / drop
|
||||
/// and right click clearing slots. Anything else is still doable.
|
||||
/// </summary>
|
||||
public bool Locked { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// All the action slots in order.
|
||||
/// </summary>
|
||||
public IEnumerable<ActionSlot> Slots => _slots;
|
||||
|
||||
public ActionsUI(ClientActionsComponent actionsComponent)
|
||||
{
|
||||
_actionsComponent = actionsComponent;
|
||||
_actionManager = IoCManager.Resolve<ActionManager>();
|
||||
_entityManager = IoCManager.Resolve<IEntityManager>();
|
||||
_gameTiming = IoCManager.Resolve<IGameTiming>();
|
||||
_menu = new ActionMenu(_actionsComponent, this);
|
||||
LayoutContainer.SetGrowHorizontal(this, LayoutContainer.GrowDirection.End);
|
||||
LayoutContainer.SetGrowVertical(this, LayoutContainer.GrowDirection.End);
|
||||
LayoutContainer.SetAnchorTop(this, 0f);
|
||||
LayoutContainer.SetAnchorBottom(this, 0.8f);
|
||||
LayoutContainer.SetMarginLeft(this, 10);
|
||||
LayoutContainer.SetMarginTop(this, 100);
|
||||
|
||||
SizeFlagsHorizontal = SizeFlags.None;
|
||||
SizeFlagsVertical = SizeFlags.FillExpand;
|
||||
|
||||
var resourceCache = IoCManager.Resolve<IResourceCache>();
|
||||
|
||||
// everything needs to go within an inner panel container so the panel resizes to fit the elements.
|
||||
// Because ActionsUI is being anchored by layoutcontainer, the hotbar backing would appear too tall
|
||||
// if ActionsUI was the panel container
|
||||
|
||||
var panelContainer = new PanelContainer()
|
||||
{
|
||||
StyleClasses = {StyleNano.StyleClassHotbarPanel},
|
||||
SizeFlagsHorizontal = SizeFlags.None,
|
||||
SizeFlagsVertical = SizeFlags.None
|
||||
};
|
||||
AddChild(panelContainer);
|
||||
|
||||
var hotbarContainer = new VBoxContainer
|
||||
{
|
||||
SeparationOverride = 3,
|
||||
SizeFlagsHorizontal = SizeFlags.None
|
||||
};
|
||||
panelContainer.AddChild(hotbarContainer);
|
||||
|
||||
var settingsContainer = new HBoxContainer
|
||||
{
|
||||
SizeFlagsHorizontal = SizeFlags.FillExpand
|
||||
};
|
||||
hotbarContainer.AddChild(settingsContainer);
|
||||
|
||||
settingsContainer.AddChild(new Control { SizeFlagsHorizontal = SizeFlags.FillExpand, SizeFlagsStretchRatio = 1 });
|
||||
_lockTexture = resourceCache.GetTexture("/Textures/Interface/Nano/lock.svg.png");
|
||||
_unlockTexture = resourceCache.GetTexture("/Textures/Interface/Nano/lock_open.svg.png");
|
||||
_lockButton = new TextureButton
|
||||
{
|
||||
TextureNormal = _unlockTexture,
|
||||
SizeFlagsHorizontal = SizeFlags.ShrinkCenter,
|
||||
SizeFlagsVertical = SizeFlags.ShrinkCenter,
|
||||
SizeFlagsStretchRatio = 1
|
||||
};
|
||||
settingsContainer.AddChild(_lockButton);
|
||||
settingsContainer.AddChild(new Control { SizeFlagsHorizontal = SizeFlags.FillExpand, SizeFlagsStretchRatio = 2 });
|
||||
_settingsButton = new TextureButton
|
||||
{
|
||||
TextureNormal = resourceCache.GetTexture("/Textures/Interface/Nano/gear.svg.png"),
|
||||
SizeFlagsHorizontal = SizeFlags.ShrinkCenter,
|
||||
SizeFlagsVertical = SizeFlags.ShrinkCenter,
|
||||
SizeFlagsStretchRatio = 1
|
||||
};
|
||||
settingsContainer.AddChild(_settingsButton);
|
||||
settingsContainer.AddChild(new Control { SizeFlagsHorizontal = SizeFlags.FillExpand, SizeFlagsStretchRatio = 1 });
|
||||
|
||||
// this allows a 2 column layout if window gets too small
|
||||
_slotContainer = new GridContainer
|
||||
{
|
||||
MaxHeight = CalcMaxHeight()
|
||||
};
|
||||
hotbarContainer.AddChild(_slotContainer);
|
||||
|
||||
var loadoutContainer = new HBoxContainer
|
||||
{
|
||||
SizeFlagsHorizontal = SizeFlags.FillExpand
|
||||
};
|
||||
hotbarContainer.AddChild(loadoutContainer);
|
||||
|
||||
loadoutContainer.AddChild(new Control { SizeFlagsHorizontal = SizeFlags.FillExpand, SizeFlagsStretchRatio = 1 });
|
||||
_previousHotbarButton = new TextureButton
|
||||
{
|
||||
TextureNormal = 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 });
|
||||
_loadoutNumber = new Label
|
||||
{
|
||||
Text = "1",
|
||||
SizeFlagsStretchRatio = 1
|
||||
};
|
||||
loadoutContainer.AddChild(_loadoutNumber);
|
||||
loadoutContainer.AddChild(new Control { SizeFlagsHorizontal = SizeFlags.FillExpand, SizeFlagsStretchRatio = 2 });
|
||||
_nextHotbarButton = new TextureButton
|
||||
{
|
||||
TextureNormal = 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 });
|
||||
|
||||
_slots = new ActionSlot[ClientActionsComponent.Slots];
|
||||
|
||||
_dragShadow = new TextureRect
|
||||
{
|
||||
CustomMinimumSize = (64, 64),
|
||||
Stretch = TextureRect.StretchMode.Scale,
|
||||
Visible = false
|
||||
};
|
||||
UserInterfaceManager.PopupRoot.AddChild(_dragShadow);
|
||||
LayoutContainer.SetSize(_dragShadow, (64, 64));
|
||||
|
||||
for (byte i = 0; i < ClientActionsComponent.Slots; i++)
|
||||
{
|
||||
var slot = new ActionSlot(this, actionsComponent, i);
|
||||
_slotContainer.AddChild(slot);
|
||||
_slots[i] = slot;
|
||||
}
|
||||
|
||||
DragDropHelper = new DragDropHelper<ActionSlot>(OnBeginActionDrag, OnContinueActionDrag, OnEndActionDrag);
|
||||
}
|
||||
|
||||
protected override void EnteredTree()
|
||||
{
|
||||
base.EnteredTree();
|
||||
_lockButton.OnPressed += OnLockPressed;
|
||||
_nextHotbarButton.OnPressed += NextHotbar;
|
||||
_previousHotbarButton.OnPressed += PreviousHotbar;
|
||||
_settingsButton.OnPressed += OnToggleActionsMenu;
|
||||
}
|
||||
|
||||
protected override void ExitedTree()
|
||||
{
|
||||
base.ExitedTree();
|
||||
StopTargeting();
|
||||
_menu.Close();
|
||||
_lockButton.OnPressed -= OnLockPressed;
|
||||
_nextHotbarButton.OnPressed -= NextHotbar;
|
||||
_previousHotbarButton.OnPressed -= PreviousHotbar;
|
||||
_settingsButton.OnPressed -= OnToggleActionsMenu;
|
||||
}
|
||||
|
||||
protected override Vector2 CalculateMinimumSize()
|
||||
{
|
||||
// allows us to shrink down to a 2-column layout minimum
|
||||
return (10, 400);
|
||||
}
|
||||
|
||||
protected override void Resized()
|
||||
{
|
||||
base.Resized();
|
||||
_slotContainer.MaxHeight = CalcMaxHeight();
|
||||
}
|
||||
|
||||
private float CalcMaxHeight()
|
||||
{
|
||||
// TODO: Can rework this once https://github.com/space-wizards/RobustToolbox/issues/1392 is done,
|
||||
// this is here because there isn't currently a good way to allow the grid to adjust its height based
|
||||
// on constraints, otherwise we would use anchors to lay it out
|
||||
|
||||
// it looks bad to have an uneven number of slots in the columns,
|
||||
// so we either do a single column or 2 equal sized columns
|
||||
if (Height < 650)
|
||||
{
|
||||
// 2 column
|
||||
return 400;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 1 column
|
||||
return 900;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void UIScaleChanged()
|
||||
{
|
||||
_slotContainer.MaxHeight = CalcMaxHeight();
|
||||
base.UIScaleChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refresh the display of all the slots in the currently displayed hotbar,
|
||||
/// to reflect the current component state and assignments of actions component.
|
||||
/// </summary>
|
||||
public void UpdateUI()
|
||||
{
|
||||
_menu.UpdateUI();
|
||||
|
||||
foreach (var actionSlot in Slots)
|
||||
{
|
||||
var assignedActionType = _actionsComponent.Assignments[SelectedHotbar, actionSlot.SlotIndex];
|
||||
if (!assignedActionType.HasValue)
|
||||
{
|
||||
actionSlot.Clear();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (assignedActionType.Value.TryGetAction(out var actionType))
|
||||
{
|
||||
UpdateActionSlot(actionType, actionSlot, assignedActionType);
|
||||
}
|
||||
else if (assignedActionType.Value.TryGetItemActionWithoutItem(out var itemlessActionType))
|
||||
{
|
||||
UpdateActionSlot(itemlessActionType, actionSlot, assignedActionType);
|
||||
}
|
||||
else if (assignedActionType.Value.TryGetItemActionWithItem(out var itemActionType, out var item))
|
||||
{
|
||||
UpdateActionSlot(item, itemActionType, actionSlot, assignedActionType);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.ErrorS("action", "unexpected Assignment type {0}",
|
||||
assignedActionType.Value.Assignment);
|
||||
actionSlot.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateActionSlot(ActionType actionType, ActionSlot actionSlot, ActionAssignment? assignedActionType)
|
||||
{
|
||||
if (_actionManager.TryGet(actionType, out var action))
|
||||
{
|
||||
actionSlot.Assign(action, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.ErrorS("action", "unrecognized actionType {0}", assignedActionType);
|
||||
actionSlot.Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_actionsComponent.TryGetActionState(actionType, out var actionState) || !actionState.Enabled)
|
||||
{
|
||||
// action is currently disabled
|
||||
|
||||
// just revoked an action we were trying to target with, stop targeting
|
||||
if (SelectingTargetFor?.Action != null && SelectingTargetFor.Action == action)
|
||||
{
|
||||
StopTargeting();
|
||||
}
|
||||
|
||||
actionSlot.DisableAction();
|
||||
actionSlot.Cooldown = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
// action is currently granted
|
||||
actionSlot.EnableAction();
|
||||
actionSlot.Cooldown = actionState.Cooldown;
|
||||
|
||||
// if we are targeting with an action now on cooldown, stop targeting
|
||||
if (SelectingTargetFor?.Action != null && SelectingTargetFor.Action == action &&
|
||||
actionState.IsOnCooldown(_gameTiming))
|
||||
{
|
||||
StopTargeting();
|
||||
}
|
||||
}
|
||||
|
||||
// check if we need to toggle it
|
||||
if (action.BehaviorType == BehaviorType.Toggle)
|
||||
{
|
||||
actionSlot.ToggledOn = actionState.ToggledOn;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateActionSlot(ItemActionType itemlessActionType, ActionSlot actionSlot,
|
||||
ActionAssignment? assignedActionType)
|
||||
{
|
||||
if (_actionManager.TryGet(itemlessActionType, out var action))
|
||||
{
|
||||
actionSlot.Assign(action);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.ErrorS("action", "unrecognized actionType {0}", assignedActionType);
|
||||
actionSlot.Clear();
|
||||
}
|
||||
actionSlot.Cooldown = null;
|
||||
}
|
||||
|
||||
private void UpdateActionSlot(EntityUid item, ItemActionType itemActionType, ActionSlot actionSlot,
|
||||
ActionAssignment? assignedActionType)
|
||||
{
|
||||
if (!_entityManager.TryGetEntity(item, out var itemEntity)) return;
|
||||
if (_actionManager.TryGet(itemActionType, out var action))
|
||||
{
|
||||
actionSlot.Assign(action, itemEntity, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.ErrorS("action", "unrecognized actionType {0}", assignedActionType);
|
||||
actionSlot.Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_actionsComponent.TryGetItemActionState(itemActionType, item, out var actionState))
|
||||
{
|
||||
// action is no longer tied to an item, this should never happen as we
|
||||
// check this at the start of this method. But just to be safe
|
||||
// we will restore our assignment here to the correct state
|
||||
Logger.ErrorS("action", "coding error, expected actionType {0} to have" +
|
||||
" a state but it didn't", assignedActionType);
|
||||
_actionsComponent.Assignments.AssignSlot(SelectedHotbar, actionSlot.SlotIndex,
|
||||
ActionAssignment.For(itemActionType));
|
||||
actionSlot.Assign(action);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!actionState.Enabled)
|
||||
{
|
||||
// just disabled an action we were trying to target with, stop targeting
|
||||
if (SelectingTargetFor?.Action != null && SelectingTargetFor.Action == action)
|
||||
{
|
||||
StopTargeting();
|
||||
}
|
||||
|
||||
actionSlot.DisableAction();
|
||||
}
|
||||
else
|
||||
{
|
||||
// action is currently granted
|
||||
actionSlot.EnableAction();
|
||||
|
||||
// if we are targeting with an action now on cooldown, stop targeting
|
||||
if (SelectingTargetFor?.Action != null && SelectingTargetFor.Action == action &&
|
||||
SelectingTargetFor.Item == itemEntity &&
|
||||
actionState.IsOnCooldown(_gameTiming))
|
||||
{
|
||||
StopTargeting();
|
||||
}
|
||||
}
|
||||
actionSlot.Cooldown = actionState.Cooldown;
|
||||
|
||||
// check if we need to toggle it
|
||||
if (action.BehaviorType == BehaviorType.Toggle)
|
||||
{
|
||||
actionSlot.ToggledOn = actionState.ToggledOn;
|
||||
}
|
||||
}
|
||||
|
||||
private void NextHotbar(BaseButton.ButtonEventArgs args)
|
||||
{
|
||||
ChangeHotbar((byte) ((SelectedHotbar + 1) % ClientActionsComponent.Hotbars));
|
||||
}
|
||||
|
||||
private void PreviousHotbar(BaseButton.ButtonEventArgs args)
|
||||
{
|
||||
var newBar = SelectedHotbar == 0 ? ClientActionsComponent.Hotbars - 1 : SelectedHotbar - 1;
|
||||
ChangeHotbar((byte) newBar);
|
||||
}
|
||||
|
||||
|
||||
private void ChangeHotbar(byte hotbar)
|
||||
{
|
||||
StopTargeting();
|
||||
SelectedHotbar = hotbar;
|
||||
_loadoutNumber.Text = (hotbar + 1).ToString();
|
||||
UpdateUI();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If currently targeting with this slot, stops targeting.
|
||||
/// If currently targeting with no slot or a different slot, switches to
|
||||
/// targeting with the specified slot.
|
||||
/// </summary>
|
||||
/// <param name="slot"></param>
|
||||
public void ToggleTargeting(ActionSlot slot)
|
||||
{
|
||||
if (SelectingTargetFor == slot)
|
||||
{
|
||||
StopTargeting();
|
||||
return;
|
||||
}
|
||||
StartTargeting(slot);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Puts us in targeting mode, where we need to pick either a target point or entity
|
||||
/// </summary>
|
||||
private void StartTargeting(ActionSlot actionSlot)
|
||||
{
|
||||
// If we were targeting something else we should stop
|
||||
StopTargeting();
|
||||
|
||||
SelectingTargetFor = actionSlot;
|
||||
|
||||
// show it as toggled on to indicate we are currently selecting a target for it
|
||||
if (!actionSlot.ToggledOn)
|
||||
{
|
||||
actionSlot.ToggledOn = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Switch out of targeting mode if currently selecting target for an action
|
||||
/// </summary>
|
||||
public void StopTargeting()
|
||||
{
|
||||
if (SelectingTargetFor == null) return;
|
||||
if (SelectingTargetFor.ToggledOn)
|
||||
{
|
||||
SelectingTargetFor.ToggledOn = false;
|
||||
}
|
||||
SelectingTargetFor = null;
|
||||
}
|
||||
|
||||
private void OnToggleActionsMenu(BaseButton.ButtonEventArgs args)
|
||||
{
|
||||
ToggleActionsMenu();
|
||||
}
|
||||
|
||||
public void ToggleActionsMenu()
|
||||
{
|
||||
if (_menu.IsOpen)
|
||||
{
|
||||
_menu.Close();
|
||||
}
|
||||
else
|
||||
{
|
||||
_menu.OpenCentered();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnLockPressed(BaseButton.ButtonEventArgs obj)
|
||||
{
|
||||
Locked = !Locked;
|
||||
_lockButton.TextureNormal = Locked ? _lockTexture : _unlockTexture;
|
||||
}
|
||||
|
||||
private bool OnBeginActionDrag()
|
||||
{
|
||||
// only initiate the drag if the slot has an action in it
|
||||
if (Locked || DragDropHelper.Dragged.Action == null) return false;
|
||||
|
||||
_dragShadow.Texture = DragDropHelper.Dragged.Action.Icon.Frame0();
|
||||
LayoutContainer.SetPosition(_dragShadow, UserInterfaceManager.MousePositionScaled - (32, 32));
|
||||
DragDropHelper.Dragged.CancelPress();
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool OnContinueActionDrag(float frameTime)
|
||||
{
|
||||
// stop if there's no action in the slot
|
||||
if (Locked || DragDropHelper.Dragged.Action == null) return false;
|
||||
|
||||
// keep dragged entity centered under mouse
|
||||
LayoutContainer.SetPosition(_dragShadow, UserInterfaceManager.MousePositionScaled - (32, 32));
|
||||
// we don't set this visible until frameupdate, otherwise it flickers
|
||||
_dragShadow.Visible = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnEndActionDrag()
|
||||
{
|
||||
_dragShadow.Visible = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle keydown / keyup for one of the slots via a keybinding, simulates mousedown/mouseup on it.
|
||||
/// </summary>
|
||||
/// <param name="slot">slot index to to receive the press (0 corresponds to the one labeled 1, 9 corresponds to the one labeled 0)</param>
|
||||
public void HandleHotbarKeybind(byte slot, PointerInputCmdHandler.PointerInputCmdArgs args)
|
||||
{
|
||||
var actionSlot = _slots[slot];
|
||||
actionSlot.Depress(args.State == BoundKeyState.Down);
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.Update(args);
|
||||
DragDropHelper.Update(args.DeltaSeconds);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
using System;
|
||||
using Content.Client.UserInterface.Stylesheets;
|
||||
using Robust.Client.Graphics.Drawing;
|
||||
using Robust.Client.Interfaces.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Content.Client.UserInterface
|
||||
@@ -16,58 +14,51 @@ namespace Content.Client.UserInterface
|
||||
{
|
||||
public GridContainer Grid { get; }
|
||||
|
||||
private readonly IClyde _clyde;
|
||||
|
||||
public AlertsUI(IClyde clyde)
|
||||
public AlertsUI()
|
||||
{
|
||||
_clyde = clyde;
|
||||
LayoutContainer.SetGrowHorizontal(this, LayoutContainer.GrowDirection.Begin);
|
||||
LayoutContainer.SetGrowVertical(this, LayoutContainer.GrowDirection.End);
|
||||
LayoutContainer.SetAnchorTop(this, 0f);
|
||||
LayoutContainer.SetAnchorRight(this, 1f);
|
||||
LayoutContainer.SetAnchorBottom(this, 1f);
|
||||
LayoutContainer.SetMarginBottom(this, -180);
|
||||
LayoutContainer.SetMarginTop(this, 250);
|
||||
LayoutContainer.SetMarginRight(this, -10);
|
||||
var panelContainer = new PanelContainer
|
||||
{
|
||||
StyleClasses = {StyleNano.StyleClassTransparentBorderedWindowPanel},
|
||||
SizeFlagsVertical = SizeFlags.FillExpand,
|
||||
SizeFlagsHorizontal = SizeFlags.ShrinkEnd,
|
||||
SizeFlagsVertical = SizeFlags.None
|
||||
};
|
||||
AddChild(panelContainer);
|
||||
|
||||
Grid = new GridContainer
|
||||
{
|
||||
MaxHeight = CalcMaxHeight(clyde.ScreenSize),
|
||||
MaxHeight = 64,
|
||||
ExpandBackwards = true
|
||||
};
|
||||
panelContainer.AddChild(Grid);
|
||||
clyde.OnWindowResized += ClydeOnOnWindowResized;
|
||||
|
||||
LayoutContainer.SetGrowHorizontal(this, LayoutContainer.GrowDirection.Begin);
|
||||
LayoutContainer.SetAnchorAndMarginPreset(this, LayoutContainer.LayoutPreset.TopRight, margin: 10);
|
||||
LayoutContainer.SetMarginTop(this, 250);
|
||||
}
|
||||
|
||||
protected override void UIScaleChanged()
|
||||
{
|
||||
Grid.MaxHeight = CalcMaxHeight(_clyde.ScreenSize);
|
||||
base.UIScaleChanged();
|
||||
}
|
||||
|
||||
private void ClydeOnOnWindowResized(WindowResizedEventArgs obj)
|
||||
protected override void Resized()
|
||||
{
|
||||
// TODO: Can rework this once https://github.com/space-wizards/RobustToolbox/issues/1392 is done,
|
||||
// this is here because there isn't currently a good way to allow the grid to adjust its height based
|
||||
// on constraints, otherwise we would use anchors to lay it out
|
||||
Grid.MaxHeight = CalcMaxHeight(obj.NewSize);;
|
||||
base.Resized();
|
||||
Grid.MaxHeight = Height;
|
||||
}
|
||||
|
||||
private float CalcMaxHeight(Vector2i screenSize)
|
||||
protected override Vector2 CalculateMinimumSize()
|
||||
{
|
||||
return Math.Max(((screenSize.Y) / UIScale) - 420, 1);
|
||||
// allows us to shrink down to a single row
|
||||
return (64, 64);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
protected override void UIScaleChanged()
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
_clyde.OnWindowResized -= ClydeOnOnWindowResized;
|
||||
}
|
||||
Grid.MaxHeight = Height;
|
||||
base.UIScaleChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
652
Content.Client/UserInterface/Controls/ActionSlot.cs
Normal file
652
Content.Client/UserInterface/Controls/ActionSlot.cs
Normal file
@@ -0,0 +1,652 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using Content.Client.GameObjects.Components.Mobs;
|
||||
using Content.Client.UserInterface.Stylesheets;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.GameObjects.Components.Mobs;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Interfaces.GameObjects.Components;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Interfaces.Timing;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.UserInterface.Controls
|
||||
{
|
||||
/// <summary>
|
||||
/// A slot in the action hotbar. Not extending BaseButton because
|
||||
/// its needs diverged too much.
|
||||
/// </summary>
|
||||
public class ActionSlot : PanelContainer
|
||||
{
|
||||
// shorter than default tooltip delay so user can more easily
|
||||
// see what actions they've been given
|
||||
private const float CustomTooltipDelay = 0.5f;
|
||||
|
||||
private static readonly string EnabledColor = "#7b7e9e";
|
||||
private static readonly string DisabledColor = "#950000";
|
||||
|
||||
/// <summary>
|
||||
/// Current action in this slot.
|
||||
/// </summary>
|
||||
public BaseActionPrototype? Action { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// true if there is an action assigned to the slot
|
||||
/// </summary>
|
||||
public bool HasAssignment => Action != null;
|
||||
|
||||
private bool HasToggleSprite => Action != null && Action.IconOn != SpriteSpecifier.Invalid;
|
||||
|
||||
/// <summary>
|
||||
/// Only applicable when an action is in this slot.
|
||||
/// True if the action is currently shown as enabled, false if action disabled.
|
||||
/// </summary>
|
||||
public bool ActionEnabled { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Is there an action in the slot that can currently be used?
|
||||
/// </summary>
|
||||
public bool CanUseAction => HasAssignment && ActionEnabled && !IsOnCooldown;
|
||||
|
||||
/// <summary>
|
||||
/// Item the action is provided by, only valid if Action is an ItemActionPrototype. May be null
|
||||
/// if the item action is not yet tied to an item.
|
||||
/// </summary>
|
||||
public IEntity? Item { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the action in this slot should be shown as toggled on. Separate from Depressed.
|
||||
/// </summary>
|
||||
public bool ToggledOn
|
||||
{
|
||||
get => _toggledOn;
|
||||
set
|
||||
{
|
||||
if (_toggledOn == value) return;
|
||||
_toggledOn = value;
|
||||
UpdateIcons();
|
||||
DrawModeChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 1-10 corresponding to the number label on the slot (10 is labeled as 0)
|
||||
/// </summary>
|
||||
private byte SlotNumber => (byte) (SlotIndex + 1);
|
||||
public byte SlotIndex { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Current cooldown displayed in this slot. Set to null to show no cooldown.
|
||||
/// </summary>
|
||||
public (TimeSpan Start, TimeSpan End)? Cooldown
|
||||
{
|
||||
get => _cooldown;
|
||||
set
|
||||
{
|
||||
_cooldown = value;
|
||||
if (SuppliedTooltip is ActionAlertTooltip actionAlertTooltip)
|
||||
{
|
||||
actionAlertTooltip.Cooldown = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
private (TimeSpan Start, TimeSpan End)? _cooldown;
|
||||
|
||||
public bool IsOnCooldown => Cooldown.HasValue && _gameTiming.CurTime < Cooldown.Value.End;
|
||||
|
||||
private readonly IGameTiming _gameTiming;
|
||||
private readonly RichTextLabel _number;
|
||||
private readonly TextureRect _bigActionIcon;
|
||||
private readonly TextureRect _smallActionIcon;
|
||||
private readonly SpriteView _smallItemSpriteView;
|
||||
private readonly SpriteView _bigItemSpriteView;
|
||||
private readonly CooldownGraphic _cooldownGraphic;
|
||||
private readonly ActionsUI _actionsUI;
|
||||
private readonly ClientActionsComponent _actionsComponent;
|
||||
private bool _toggledOn;
|
||||
// whether button is currently pressed down by mouse or keybind down.
|
||||
private bool _depressed;
|
||||
private bool _beingHovered;
|
||||
|
||||
/// <summary>
|
||||
/// Creates an action slot for the specified number
|
||||
/// </summary>
|
||||
/// <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)
|
||||
{
|
||||
_actionsComponent = actionsComponent;
|
||||
_actionsUI = actionsUI;
|
||||
_gameTiming = IoCManager.Resolve<IGameTiming>();
|
||||
SlotIndex = slotIndex;
|
||||
MouseFilter = MouseFilterMode.Stop;
|
||||
|
||||
CustomMinimumSize = (64, 64);
|
||||
SizeFlagsVertical = SizeFlags.None;
|
||||
TooltipDelay = CustomTooltipDelay;
|
||||
TooltipSupplier = SupplyTooltip;
|
||||
|
||||
_number = new RichTextLabel
|
||||
{
|
||||
StyleClasses = {StyleNano.StyleClassHotbarSlotNumber}
|
||||
};
|
||||
_number.SetMessage(SlotNumberLabel());
|
||||
|
||||
_bigActionIcon = new TextureRect
|
||||
{
|
||||
SizeFlagsHorizontal = SizeFlags.FillExpand,
|
||||
SizeFlagsVertical = SizeFlags.FillExpand,
|
||||
Stretch = TextureRect.StretchMode.Scale,
|
||||
Visible = false
|
||||
};
|
||||
_bigItemSpriteView = new SpriteView
|
||||
{
|
||||
SizeFlagsHorizontal = SizeFlags.FillExpand,
|
||||
SizeFlagsVertical = SizeFlags.FillExpand,
|
||||
Scale = (2,2),
|
||||
Visible = false
|
||||
};
|
||||
_smallActionIcon = new TextureRect
|
||||
{
|
||||
SizeFlagsHorizontal = SizeFlags.ShrinkEnd,
|
||||
SizeFlagsVertical = SizeFlags.ShrinkEnd,
|
||||
Stretch = TextureRect.StretchMode.Scale,
|
||||
Visible = false
|
||||
};
|
||||
_smallItemSpriteView = new SpriteView
|
||||
{
|
||||
SizeFlagsHorizontal = SizeFlags.ShrinkEnd,
|
||||
SizeFlagsVertical = SizeFlags.ShrinkEnd,
|
||||
Visible = false
|
||||
};
|
||||
|
||||
_cooldownGraphic = new CooldownGraphic {Progress = 0, Visible = false};
|
||||
|
||||
// padding to the left of the number to shift it right
|
||||
var paddingBox = new HBoxContainer()
|
||||
{
|
||||
SizeFlagsHorizontal = SizeFlags.FillExpand,
|
||||
SizeFlagsVertical = SizeFlags.FillExpand,
|
||||
CustomMinimumSize = (64, 64)
|
||||
};
|
||||
paddingBox.AddChild(new Control()
|
||||
{
|
||||
CustomMinimumSize = (4, 4),
|
||||
SizeFlagsVertical = SizeFlags.Fill
|
||||
});
|
||||
paddingBox.AddChild(_number);
|
||||
|
||||
// padding to the left of the small icon
|
||||
var paddingBoxItemIcon = new HBoxContainer()
|
||||
{
|
||||
SizeFlagsHorizontal = SizeFlags.FillExpand,
|
||||
SizeFlagsVertical = SizeFlags.FillExpand,
|
||||
CustomMinimumSize = (64, 64)
|
||||
};
|
||||
paddingBoxItemIcon.AddChild(new Control()
|
||||
{
|
||||
CustomMinimumSize = (32, 32),
|
||||
SizeFlagsVertical = SizeFlags.Fill
|
||||
});
|
||||
paddingBoxItemIcon.AddChild(new Control
|
||||
{
|
||||
Children =
|
||||
{
|
||||
_smallActionIcon,
|
||||
_smallItemSpriteView
|
||||
}
|
||||
});
|
||||
AddChild(_bigActionIcon);
|
||||
AddChild(_bigItemSpriteView);
|
||||
AddChild(_cooldownGraphic);
|
||||
AddChild(paddingBox);
|
||||
AddChild(paddingBoxItemIcon);
|
||||
DrawModeChanged();
|
||||
}
|
||||
|
||||
private Control? SupplyTooltip(Control sender)
|
||||
{
|
||||
return Action == null ? null :
|
||||
new ActionAlertTooltip(Action.Name, Action.Description, Action.Requires) {Cooldown = Cooldown};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Action attempt for performing the action in the slot
|
||||
/// </summary>
|
||||
public IActionAttempt? ActionAttempt()
|
||||
{
|
||||
IActionAttempt? attempt = Action switch
|
||||
{
|
||||
ActionPrototype actionPrototype => new ActionAttempt(actionPrototype),
|
||||
ItemActionPrototype itemActionPrototype =>
|
||||
(Item != null && Item.TryGetComponent<ItemActionsComponent>(out var itemActions)) ?
|
||||
new ItemActionAttempt(itemActionPrototype, Item, itemActions) : null,
|
||||
_ => null
|
||||
};
|
||||
return attempt;
|
||||
}
|
||||
|
||||
protected override void MouseEntered()
|
||||
{
|
||||
base.MouseEntered();
|
||||
|
||||
_beingHovered = true;
|
||||
DrawModeChanged();
|
||||
if (Action is not ItemActionPrototype) return;
|
||||
if (Item == null) return;
|
||||
_actionsComponent.HighlightItemSlot(Item);
|
||||
}
|
||||
|
||||
protected override void MouseExited()
|
||||
{
|
||||
base.MouseExited();
|
||||
_beingHovered = false;
|
||||
CancelPress();
|
||||
DrawModeChanged();
|
||||
_actionsComponent.StopHighlightingItemSlots();
|
||||
}
|
||||
|
||||
protected override void KeyBindDown(GUIBoundKeyEventArgs args)
|
||||
{
|
||||
base.KeyBindDown(args);
|
||||
|
||||
if (args.Function == EngineKeyFunctions.UIRightClick)
|
||||
{
|
||||
if (!_actionsUI.Locked && !_actionsUI.DragDropHelper.IsDragging)
|
||||
{
|
||||
_actionsComponent.Assignments.ClearSlot(_actionsUI.SelectedHotbar, SlotIndex, true);
|
||||
_actionsUI.StopTargeting();
|
||||
_actionsUI.UpdateUI();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// only handle clicks, and can't do anything to this if no assignment
|
||||
if (args.Function != EngineKeyFunctions.UIClick || !HasAssignment)
|
||||
return;
|
||||
|
||||
// might turn into a drag or a full press if released
|
||||
Depress(true);
|
||||
_actionsUI.DragDropHelper.MouseDown(this);
|
||||
DrawModeChanged();
|
||||
}
|
||||
|
||||
protected override void KeyBindUp(GUIBoundKeyEventArgs args)
|
||||
{
|
||||
base.KeyBindUp(args);
|
||||
|
||||
if (args.Function != EngineKeyFunctions.UIClick)
|
||||
return;
|
||||
|
||||
// might be finishing a drag or using the action
|
||||
if (_actionsUI.DragDropHelper.IsDragging &&
|
||||
_actionsUI.DragDropHelper.Dragged == this &&
|
||||
UserInterfaceManager.CurrentlyHovered is ActionSlot targetSlot &&
|
||||
targetSlot != this)
|
||||
{
|
||||
// finish the drag, swap the 2 slots
|
||||
var fromIdx = SlotIndex;
|
||||
var fromAssignment = _actionsComponent.Assignments[_actionsUI.SelectedHotbar, fromIdx];
|
||||
var toIdx = targetSlot.SlotIndex;
|
||||
var toAssignment = _actionsComponent.Assignments[_actionsUI.SelectedHotbar, toIdx];
|
||||
|
||||
if (fromIdx == toIdx) return;
|
||||
if (!fromAssignment.HasValue) return;
|
||||
|
||||
_actionsComponent.Assignments.AssignSlot(_actionsUI.SelectedHotbar, toIdx, fromAssignment.Value);
|
||||
if (toAssignment.HasValue)
|
||||
{
|
||||
_actionsComponent.Assignments.AssignSlot(_actionsUI.SelectedHotbar, fromIdx, toAssignment.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
_actionsComponent.Assignments.ClearSlot(_actionsUI.SelectedHotbar, fromIdx, false);
|
||||
}
|
||||
_actionsUI.UpdateUI();
|
||||
}
|
||||
else
|
||||
{
|
||||
// perform the action
|
||||
if (UserInterfaceManager.CurrentlyHovered == this)
|
||||
{
|
||||
Depress(false);
|
||||
}
|
||||
}
|
||||
_actionsUI.DragDropHelper.EndDrag();
|
||||
DrawModeChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancel current press without triggering the action
|
||||
/// </summary>
|
||||
public void CancelPress()
|
||||
{
|
||||
_depressed = false;
|
||||
DrawModeChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Press this button down. If it was depressed and now set to not depressed, will
|
||||
/// trigger the action. Only has an effect if CanUseAction.
|
||||
/// </summary>
|
||||
public void Depress(bool depress)
|
||||
{
|
||||
if (!CanUseAction) return;
|
||||
|
||||
if (_depressed && !depress)
|
||||
{
|
||||
// fire the action
|
||||
// no left-click interaction with it on cooldown or revoked
|
||||
_actionsComponent.AttemptAction(this);
|
||||
}
|
||||
_depressed = depress;
|
||||
DrawModeChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the action assigned to this slot.
|
||||
/// </summary>
|
||||
/// <param name="action">action to assign</param>
|
||||
/// <param name="actionEnabled">whether action should initially appear enable or disabled</param>
|
||||
public void Assign(ActionPrototype action, bool actionEnabled)
|
||||
{
|
||||
// already assigned
|
||||
if (Action != null && Action == action) return;
|
||||
|
||||
Action = action;
|
||||
Item = null;
|
||||
_depressed = false;
|
||||
ToggledOn = false;
|
||||
ActionEnabled = actionEnabled;
|
||||
Cooldown = null;
|
||||
HideTooltip();
|
||||
UpdateIcons();
|
||||
DrawModeChanged();
|
||||
_number.SetMessage(SlotNumberLabel());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the item action assigned to this slot. The action will always be shown as disabled
|
||||
/// until it is tied to a specific item.
|
||||
/// </summary>
|
||||
/// <param name="action">action to assign</param>
|
||||
public void Assign(ItemActionPrototype action)
|
||||
{
|
||||
// already assigned
|
||||
if (Action != null && Action == action && Item == null) return;
|
||||
|
||||
Action = action;
|
||||
Item = null;
|
||||
_depressed = false;
|
||||
ToggledOn = false;
|
||||
ActionEnabled = false;
|
||||
Cooldown = null;
|
||||
HideTooltip();
|
||||
UpdateIcons();
|
||||
DrawModeChanged();
|
||||
_number.SetMessage(SlotNumberLabel());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the item action assigned to this slot, tied to a specific item.
|
||||
/// </summary>
|
||||
/// <param name="action">action to assign</param>
|
||||
/// <param name="item">item the action is provided by</param>
|
||||
/// <param name="actionEnabled">whether action should initially appear enable or disabled</param>
|
||||
public void Assign(ItemActionPrototype action, IEntity item, bool actionEnabled)
|
||||
{
|
||||
// already assigned
|
||||
if (Action != null && Action == action && Item == item) return;
|
||||
|
||||
Action = action;
|
||||
Item = item;
|
||||
_depressed = false;
|
||||
ToggledOn = false;
|
||||
ActionEnabled = false;
|
||||
Cooldown = null;
|
||||
HideTooltip();
|
||||
UpdateIcons();
|
||||
DrawModeChanged();
|
||||
_number.SetMessage(SlotNumberLabel());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the action assigned to this slot
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
if (!HasAssignment) return;
|
||||
Action = null;
|
||||
Item = null;
|
||||
ToggledOn = false;
|
||||
_depressed = false;
|
||||
Cooldown = null;
|
||||
HideTooltip();
|
||||
UpdateIcons();
|
||||
DrawModeChanged();
|
||||
_number.SetMessage(SlotNumberLabel());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Display the action in this slot (if there is one) as enabled
|
||||
/// </summary>
|
||||
public void EnableAction()
|
||||
{
|
||||
if (ActionEnabled || !HasAssignment) return;
|
||||
|
||||
ActionEnabled = true;
|
||||
_depressed = false;
|
||||
DrawModeChanged();
|
||||
_number.SetMessage(SlotNumberLabel());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Display the action in this slot (if there is one) as disabled.
|
||||
/// The slot is still clickable.
|
||||
/// </summary>
|
||||
public void DisableAction()
|
||||
{
|
||||
if (!ActionEnabled || !HasAssignment) return;
|
||||
|
||||
ActionEnabled = false;
|
||||
_depressed = false;
|
||||
DrawModeChanged();
|
||||
_number.SetMessage(SlotNumberLabel());
|
||||
}
|
||||
|
||||
private FormattedMessage SlotNumberLabel()
|
||||
{
|
||||
if (SlotNumber > 10) return FormattedMessage.FromMarkup("");
|
||||
var number = Loc.GetString(SlotNumber == 10 ? "0" : SlotNumber.ToString());
|
||||
var color = (ActionEnabled || !HasAssignment) ? EnabledColor : DisabledColor;
|
||||
return FormattedMessage.FromMarkup("[color=" + color + "]" + number + "[/color]");
|
||||
}
|
||||
|
||||
private void UpdateIcons()
|
||||
{
|
||||
if (!HasAssignment)
|
||||
{
|
||||
SetActionIcon(null);
|
||||
SetItemIcon(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (HasToggleSprite && ToggledOn && Action != null)
|
||||
{
|
||||
SetActionIcon(Action.IconOn.Frame0());
|
||||
}
|
||||
else if (Action != null)
|
||||
{
|
||||
SetActionIcon(Action.Icon.Frame0());
|
||||
}
|
||||
|
||||
if (Item != null)
|
||||
{
|
||||
SetItemIcon(Item.TryGetComponent<ISpriteComponent>(out var spriteComponent) ? spriteComponent : null);
|
||||
}
|
||||
else
|
||||
{
|
||||
SetItemIcon(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetActionIcon(Texture? texture)
|
||||
{
|
||||
if (texture == null || !HasAssignment)
|
||||
{
|
||||
_bigActionIcon.Texture = null;
|
||||
_bigActionIcon.Visible = false;
|
||||
_smallActionIcon.Texture = null;
|
||||
_smallActionIcon.Visible = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Action is ItemActionPrototype {IconStyle: ItemActionIconStyle.BigItem})
|
||||
{
|
||||
_bigActionIcon.Texture = null;
|
||||
_bigActionIcon.Visible = false;
|
||||
_smallActionIcon.Texture = texture;
|
||||
_smallActionIcon.Visible = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_bigActionIcon.Texture = texture;
|
||||
_bigActionIcon.Visible = true;
|
||||
_smallActionIcon.Texture = null;
|
||||
_smallActionIcon.Visible = false;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private void SetItemIcon(ISpriteComponent? sprite)
|
||||
{
|
||||
if (sprite == null || !HasAssignment)
|
||||
{
|
||||
_bigItemSpriteView.Visible = false;
|
||||
_bigItemSpriteView.Sprite = null;
|
||||
_smallItemSpriteView.Visible = false;
|
||||
_smallItemSpriteView.Sprite = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Action is ItemActionPrototype actionPrototype)
|
||||
{
|
||||
switch (actionPrototype.IconStyle)
|
||||
{
|
||||
case ItemActionIconStyle.BigItem:
|
||||
{
|
||||
_bigItemSpriteView.Visible = true;
|
||||
_bigItemSpriteView.Sprite = sprite;
|
||||
_smallItemSpriteView.Visible = false;
|
||||
_smallItemSpriteView.Sprite = null;
|
||||
break;
|
||||
}
|
||||
case ItemActionIconStyle.BigAction:
|
||||
{
|
||||
_bigItemSpriteView.Visible = false;
|
||||
_bigItemSpriteView.Sprite = null;
|
||||
_smallItemSpriteView.Visible = true;
|
||||
_smallItemSpriteView.Sprite = sprite;
|
||||
break;
|
||||
}
|
||||
case ItemActionIconStyle.NoItem:
|
||||
{
|
||||
_bigItemSpriteView.Visible = false;
|
||||
_bigItemSpriteView.Sprite = null;
|
||||
_smallItemSpriteView.Visible = false;
|
||||
_smallItemSpriteView.Sprite = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
_bigItemSpriteView.Visible = false;
|
||||
_bigItemSpriteView.Sprite = null;
|
||||
_smallItemSpriteView.Visible = false;
|
||||
_smallItemSpriteView.Sprite = null;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void DrawModeChanged()
|
||||
{
|
||||
// always show the normal empty button style if no action in this slot
|
||||
if (!HasAssignment)
|
||||
{
|
||||
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassNormal);
|
||||
return;
|
||||
}
|
||||
|
||||
// it's only depress-able if it's usable, so if we're depressed
|
||||
// show the depressed style
|
||||
if (_depressed)
|
||||
{
|
||||
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassPressed);
|
||||
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)
|
||||
{
|
||||
// when there's a toggle sprite, we're showing that sprite instead of highlighting this slot
|
||||
SetOnlyStylePseudoClass(HasToggleSprite ? ContainerButton.StylePseudoClassNormal :
|
||||
ContainerButton.StylePseudoClassPressed);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (!ActionEnabled)
|
||||
{
|
||||
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassDisabled);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassNormal);
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
if (!Cooldown.HasValue)
|
||||
{
|
||||
_cooldownGraphic.Visible = false;
|
||||
_cooldownGraphic.Progress = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
var duration = Cooldown.Value.End - Cooldown.Value.Start;
|
||||
var curTime = _gameTiming.CurTime;
|
||||
var length = duration.TotalSeconds;
|
||||
var progress = (curTime - Cooldown.Value.Start).TotalSeconds / length;
|
||||
var ratio = (progress <= 1 ? (1 - progress) : (curTime - Cooldown.Value.End).TotalSeconds * -5);
|
||||
|
||||
_cooldownGraphic.Progress = MathHelper.Clamp((float)ratio, -1, 1);
|
||||
_cooldownGraphic.Visible = ratio > -1f;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +1,48 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using Content.Client.UserInterface;
|
||||
using Content.Client.Utility;
|
||||
using Content.Shared.Alert;
|
||||
using Robust.Client.Interfaces.ResourceManagement;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Interfaces.Timing;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.GameObjects.Components.Mobs
|
||||
namespace Content.Client.UserInterface.Controls
|
||||
{
|
||||
public class AlertControl : BaseButton
|
||||
{
|
||||
// shorter than default tooltip delay so user can more easily
|
||||
// see what alerts they have
|
||||
private const float CustomTooltipDelay = 0.5f;
|
||||
|
||||
public AlertPrototype Alert { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Total duration of the cooldown in seconds. Null if no duration / cooldown.
|
||||
/// Current cooldown displayed in this slot. Set to null to show no cooldown.
|
||||
/// </summary>
|
||||
public int? TotalDuration { get; set; }
|
||||
public (TimeSpan Start, TimeSpan End)? Cooldown
|
||||
{
|
||||
get => _cooldown;
|
||||
set
|
||||
{
|
||||
_cooldown = value;
|
||||
if (SuppliedTooltip is ActionAlertTooltip actionAlertTooltip)
|
||||
{
|
||||
actionAlertTooltip.Cooldown = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
private (TimeSpan Start, TimeSpan End)? _cooldown;
|
||||
|
||||
private short? _severity;
|
||||
private readonly IGameTiming _gameTiming;
|
||||
private readonly TextureRect _icon;
|
||||
private readonly CooldownGraphic _cooldownGraphic;
|
||||
|
||||
private readonly IResourceCache _resourceCache;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Creates an alert control reflecting the indicated alert + state
|
||||
/// </summary>
|
||||
@@ -33,6 +51,9 @@ namespace Content.Client.GameObjects.Components.Mobs
|
||||
/// <param name="resourceCache">resourceCache to use to load alert icon textures</param>
|
||||
public AlertControl(AlertPrototype alert, short? severity, IResourceCache resourceCache)
|
||||
{
|
||||
_gameTiming = IoCManager.Resolve<IGameTiming>();
|
||||
TooltipDelay = CustomTooltipDelay;
|
||||
TooltipSupplier = SupplyTooltip;
|
||||
_resourceCache = resourceCache;
|
||||
Alert = alert;
|
||||
_severity = severity;
|
||||
@@ -49,6 +70,11 @@ namespace Content.Client.GameObjects.Components.Mobs
|
||||
|
||||
}
|
||||
|
||||
private Control SupplyTooltip(Control? sender)
|
||||
{
|
||||
return new ActionAlertTooltip(Alert.Name, Alert.Description) {Cooldown = Cooldown};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Change the alert severity, changing the displayed icon
|
||||
/// </summary>
|
||||
@@ -61,33 +87,24 @@ namespace Content.Client.GameObjects.Components.Mobs
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the displayed cooldown amount, doing nothing if alertCooldown is null
|
||||
/// </summary>
|
||||
/// <param name="alertCooldown">cooldown start and end</param>
|
||||
/// <param name="curTime">current game time</param>
|
||||
public void UpdateCooldown((TimeSpan Start, TimeSpan End)? alertCooldown, in TimeSpan curTime)
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
if (!alertCooldown.HasValue)
|
||||
base.FrameUpdate(args);
|
||||
if (!Cooldown.HasValue)
|
||||
{
|
||||
_cooldownGraphic.Progress = 0;
|
||||
_cooldownGraphic.Visible = false;
|
||||
TotalDuration = null;
|
||||
_cooldownGraphic.Progress = 0;
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
var start = alertCooldown.Value.Start;
|
||||
var end = alertCooldown.Value.End;
|
||||
var duration = Cooldown.Value.End - Cooldown.Value.Start;
|
||||
var curTime = _gameTiming.CurTime;
|
||||
var length = duration.TotalSeconds;
|
||||
var progress = (curTime - Cooldown.Value.Start).TotalSeconds / length;
|
||||
var ratio = (progress <= 1 ? (1 - progress) : (curTime - Cooldown.Value.End).TotalSeconds * -5);
|
||||
|
||||
var length = (end - start).TotalSeconds;
|
||||
var progress = (curTime - start).TotalSeconds / length;
|
||||
var ratio = (progress <= 1 ? (1 - progress) : (curTime - end).TotalSeconds * -5);
|
||||
|
||||
TotalDuration = (int?) Math.Round(length);
|
||||
_cooldownGraphic.Progress = MathHelper.Clamp((float)ratio, -1, 1);
|
||||
_cooldownGraphic.Visible = ratio > -1f;
|
||||
}
|
||||
_cooldownGraphic.Progress = MathHelper.Clamp((float)ratio, -1, 1);
|
||||
_cooldownGraphic.Visible = ratio > -1f;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,19 @@
|
||||
using System;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Graphics.Shaders;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.UserInterface
|
||||
{
|
||||
public class ItemSlotButton : MarginContainer
|
||||
{
|
||||
private const string HighlightShader = "SelectionOutlineInrange";
|
||||
|
||||
public TextureRect Button { get; }
|
||||
public SpriteView SpriteView { get; }
|
||||
public SpriteView HoverSpriteView { get; }
|
||||
@@ -21,9 +26,11 @@ namespace Content.Client.UserInterface
|
||||
|
||||
public bool EntityHover => HoverSpriteView.Sprite != null;
|
||||
public bool MouseIsHovering = false;
|
||||
private readonly ShaderInstance _highlightShader;
|
||||
|
||||
public ItemSlotButton(Texture texture, Texture storageTexture)
|
||||
{
|
||||
_highlightShader = IoCManager.Resolve<IPrototypeManager>().Index<ShaderPrototype>(HighlightShader).Instance();
|
||||
CustomMinimumSize = (64, 64);
|
||||
|
||||
AddChild(Button = new TextureRect
|
||||
@@ -95,6 +102,20 @@ namespace Content.Client.UserInterface
|
||||
}
|
||||
}
|
||||
|
||||
public void Highlight(bool on)
|
||||
{
|
||||
// I make no claim that this actually looks good but it's a start.
|
||||
if (on)
|
||||
{
|
||||
Button.ShaderOverride = _highlightShader;
|
||||
}
|
||||
else
|
||||
{
|
||||
Button.ShaderOverride = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void OnButtonPressed(GUIBoundKeyEventArgs args)
|
||||
{
|
||||
OnPressed?.Invoke(args);
|
||||
|
||||
@@ -150,7 +150,6 @@ namespace Content.Client.UserInterface
|
||||
AddButton(ContentKeyFunctions.ReleasePulledObject, "Release pulled object");
|
||||
AddButton(ContentKeyFunctions.Point, "Point at location");
|
||||
|
||||
|
||||
AddHeader("User Interface");
|
||||
AddButton(ContentKeyFunctions.FocusChat, "Focus chat");
|
||||
AddButton(ContentKeyFunctions.FocusOOC, "Focus chat (OOC)");
|
||||
@@ -160,6 +159,7 @@ namespace Content.Client.UserInterface
|
||||
AddButton(ContentKeyFunctions.OpenCraftingMenu, "Open crafting menu");
|
||||
AddButton(ContentKeyFunctions.OpenInventoryMenu, "Open inventory");
|
||||
AddButton(ContentKeyFunctions.OpenTutorial, "Open tutorial");
|
||||
AddButton(ContentKeyFunctions.OpenActionsMenu, "Open action menu");
|
||||
AddButton(ContentKeyFunctions.OpenEntitySpawnWindow, "Open entity spawn menu");
|
||||
AddButton(ContentKeyFunctions.OpenSandboxWindow, "Open sandbox menu");
|
||||
AddButton(ContentKeyFunctions.OpenTileSpawnWindow, "Open tile spawn menu");
|
||||
@@ -169,6 +169,18 @@ namespace Content.Client.UserInterface
|
||||
AddButton(ContentKeyFunctions.TakeScreenshot, "Take screenshot");
|
||||
AddButton(ContentKeyFunctions.TakeScreenshotNoUI, "Take screenshot (without UI)");
|
||||
|
||||
AddHeader("Hotbar");
|
||||
AddButton(ContentKeyFunctions.Hotbar1, "Hotbar slot 1");
|
||||
AddButton(ContentKeyFunctions.Hotbar2, "Hotbar slot 2");
|
||||
AddButton(ContentKeyFunctions.Hotbar3, "Hotbar slot 3");
|
||||
AddButton(ContentKeyFunctions.Hotbar4, "Hotbar slot 4");
|
||||
AddButton(ContentKeyFunctions.Hotbar5, "Hotbar slot 5");
|
||||
AddButton(ContentKeyFunctions.Hotbar6, "Hotbar slot 6");
|
||||
AddButton(ContentKeyFunctions.Hotbar7, "Hotbar slot 7");
|
||||
AddButton(ContentKeyFunctions.Hotbar8, "Hotbar slot 8");
|
||||
AddButton(ContentKeyFunctions.Hotbar9, "Hotbar slot 9");
|
||||
AddButton(ContentKeyFunctions.Hotbar0, "Hotbar slot 0");
|
||||
|
||||
AddHeader("Map Editor");
|
||||
AddButton(EngineKeyFunctions.EditorPlaceObject, "Place object");
|
||||
AddButton(EngineKeyFunctions.EditorCancelPlace, "Cancel placement");
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Linq;
|
||||
using Content.Client.GameObjects.EntitySystems;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Client.Utility;
|
||||
using Robust.Client.Graphics.Drawing;
|
||||
using Robust.Client.Interfaces.ResourceManagement;
|
||||
@@ -15,10 +16,19 @@ namespace Content.Client.UserInterface.Stylesheets
|
||||
{
|
||||
public const string StyleClassBorderedWindowPanel = "BorderedWindowPanel";
|
||||
public const string StyleClassTransparentBorderedWindowPanel = "TransparentBorderedWindowPanel";
|
||||
public const string StyleClassHotbarPanel = "HotbarPanel";
|
||||
public const string StyleClassTooltipPanel = "tooltipBox";
|
||||
public const string StyleClassTooltipAlertTitle = "tooltipAlertTitle";
|
||||
public const string StyleClassTooltipAlertDescription = "tooltipAlertDesc";
|
||||
public const string StyleClassTooltipAlertCooldown = "tooltipAlertCooldown";
|
||||
public const string StyleClassTooltipActionTitle = "tooltipActionTitle";
|
||||
public const string StyleClassTooltipActionDescription = "tooltipActionDesc";
|
||||
public const string StyleClassTooltipActionCooldown = "tooltipActionCooldown";
|
||||
public const string StyleClassTooltipActionRequirements = "tooltipActionCooldown";
|
||||
public const string StyleClassHotbarSlotNumber = "hotbarSlotNumber";
|
||||
public const string StyleClassActionSearchBox = "actionSearchBox";
|
||||
public const string StyleClassActionMenuItemRevoked = "actionMenuItemRevoked";
|
||||
|
||||
|
||||
public const string StyleClassSliderRed = "Red";
|
||||
public const string StyleClassSliderGreen = "Green";
|
||||
@@ -60,6 +70,8 @@ namespace Content.Client.UserInterface.Stylesheets
|
||||
var notoSansItalic12 = resCache.GetFont("/Fonts/NotoSans/NotoSans-Italic.ttf", 12);
|
||||
var notoSansBold12 = resCache.GetFont("/Fonts/NotoSans/NotoSans-Bold.ttf", 12);
|
||||
var notoSansDisplayBold14 = resCache.GetFont("/Fonts/NotoSansDisplay/NotoSansDisplay-Bold.ttf", 14);
|
||||
var notoSansDisplayBold16 = resCache.GetFont("/Fonts/NotoSansDisplay/NotoSansDisplay-Bold.ttf", 16);
|
||||
var notoSans15 = resCache.GetFont("/Fonts/NotoSans/NotoSans-Regular.ttf", 15);
|
||||
var notoSans16 = resCache.GetFont("/Fonts/NotoSans/NotoSans-Regular.ttf", 16);
|
||||
var notoSansBold16 = resCache.GetFont("/Fonts/NotoSans/NotoSans-Bold.ttf", 16);
|
||||
var notoSansBold18 = resCache.GetFont("/Fonts/NotoSans/NotoSans-Bold.ttf", 18);
|
||||
@@ -95,6 +107,61 @@ namespace Content.Client.UserInterface.Stylesheets
|
||||
};
|
||||
borderedTransparentWindowBackground.SetPatchMargin(StyleBox.Margin.All, 2);
|
||||
|
||||
var hotbarBackground = new StyleBoxTexture
|
||||
{
|
||||
Texture = borderedWindowBackgroundTex,
|
||||
};
|
||||
hotbarBackground.SetPatchMargin(StyleBox.Margin.All, 2);
|
||||
hotbarBackground.SetExpandMargin(StyleBox.Margin.All, 4);
|
||||
|
||||
var buttonRectTex = resCache.GetTexture("/Textures/Interface/Nano/light_panel_background_bordered.png");
|
||||
var buttonRect = new StyleBoxTexture(BaseButton)
|
||||
{
|
||||
Texture = buttonRectTex
|
||||
};
|
||||
buttonRect.SetPatchMargin(StyleBox.Margin.All, 2);
|
||||
buttonRect.SetPadding(StyleBox.Margin.All, 2);
|
||||
buttonRect.SetContentMarginOverride(StyleBox.Margin.Vertical, 2);
|
||||
buttonRect.SetContentMarginOverride(StyleBox.Margin.Horizontal, 2);
|
||||
|
||||
var buttonRectHover = new StyleBoxTexture(buttonRect)
|
||||
{
|
||||
Modulate = ButtonColorHovered
|
||||
};
|
||||
|
||||
var buttonRectPressed = new StyleBoxTexture(buttonRect)
|
||||
{
|
||||
Modulate = ButtonColorPressed
|
||||
};
|
||||
|
||||
var buttonRectDisabled = new StyleBoxTexture(buttonRect)
|
||||
{
|
||||
Modulate = ButtonColorDisabled
|
||||
};
|
||||
|
||||
var buttonRectActionMenuItemTex = resCache.GetTexture("/Textures/Interface/Nano/black_panel_light_thin_border.png");
|
||||
var buttonRectActionMenuRevokedItemTex = resCache.GetTexture("/Textures/Interface/Nano/black_panel_red_thin_border.png");
|
||||
var buttonRectActionMenuItem = new StyleBoxTexture(BaseButton)
|
||||
{
|
||||
Texture = buttonRectActionMenuItemTex
|
||||
};
|
||||
buttonRectActionMenuItem.SetPatchMargin(StyleBox.Margin.All, 2);
|
||||
buttonRectActionMenuItem.SetPadding(StyleBox.Margin.All, 2);
|
||||
buttonRectActionMenuItem.SetContentMarginOverride(StyleBox.Margin.Vertical, 2);
|
||||
buttonRectActionMenuItem.SetContentMarginOverride(StyleBox.Margin.Horizontal, 2);
|
||||
var buttonRectActionMenuItemRevoked = new StyleBoxTexture(buttonRectActionMenuItem)
|
||||
{
|
||||
Texture = buttonRectActionMenuRevokedItemTex
|
||||
};
|
||||
var buttonRectActionMenuItemHover = new StyleBoxTexture(buttonRectActionMenuItem)
|
||||
{
|
||||
Modulate = ButtonColorHovered
|
||||
};
|
||||
var buttonRectActionMenuItemPressed = new StyleBoxTexture(buttonRectActionMenuItem)
|
||||
{
|
||||
Modulate = ButtonColorPressed
|
||||
};
|
||||
|
||||
var textureInvertedTriangle = resCache.GetTexture("/Textures/Interface/Nano/inverted_triangle.svg.png");
|
||||
|
||||
var lineEditTex = resCache.GetTexture("/Textures/Interface/Nano/lineedit.png");
|
||||
@@ -105,6 +172,14 @@ namespace Content.Client.UserInterface.Stylesheets
|
||||
lineEdit.SetPatchMargin(StyleBox.Margin.All, 3);
|
||||
lineEdit.SetContentMarginOverride(StyleBox.Margin.Horizontal, 5);
|
||||
|
||||
var actionSearchBoxTex = resCache.GetTexture("/Textures/Interface/Nano/black_panel_dark_thin_border.png");
|
||||
var actionSearchBox = new StyleBoxTexture
|
||||
{
|
||||
Texture = actionSearchBoxTex,
|
||||
};
|
||||
actionSearchBox.SetPatchMargin(StyleBox.Margin.All, 3);
|
||||
actionSearchBox.SetContentMarginOverride(StyleBox.Margin.Horizontal, 5);
|
||||
|
||||
var tabContainerPanelTex = resCache.GetTexture("/Textures/Interface/Nano/tabcontainer_panel.png");
|
||||
var tabContainerPanel = new StyleBoxTexture
|
||||
{
|
||||
@@ -280,6 +355,12 @@ namespace Content.Client.UserInterface.Stylesheets
|
||||
{
|
||||
new StyleProperty(PanelContainer.StylePropertyPanel, borderedTransparentWindowBackground),
|
||||
}),
|
||||
// Hotbar background
|
||||
new StyleRule(new SelectorElement(typeof(PanelContainer), new[] {StyleClassHotbarPanel}, null, null),
|
||||
new[]
|
||||
{
|
||||
new StyleProperty(PanelContainer.StylePropertyPanel, hotbarBackground),
|
||||
}),
|
||||
// Window header.
|
||||
new StyleRule(
|
||||
new SelectorElement(typeof(PanelContainer), new[] {SS14Window.StyleClassWindowHeader}, null, null),
|
||||
@@ -376,6 +457,43 @@ namespace Content.Client.UserInterface.Stylesheets
|
||||
new StyleProperty("font-color", Color.FromHex("#E5E5E581")),
|
||||
}),
|
||||
|
||||
// action slot hotbar buttons
|
||||
new StyleRule(new SelectorElement(typeof(ActionSlot), null, null, new[] {ContainerButton.StylePseudoClassNormal}), new[]
|
||||
{
|
||||
new StyleProperty(PanelContainer.StylePropertyPanel, buttonRect),
|
||||
}),
|
||||
new StyleRule(new SelectorElement(typeof(ActionSlot), null, null, new[] {ContainerButton.StylePseudoClassHover}), new[]
|
||||
{
|
||||
new StyleProperty(PanelContainer.StylePropertyPanel, buttonRectHover),
|
||||
}),
|
||||
new StyleRule(new SelectorElement(typeof(ActionSlot), null, null, new[] {ContainerButton.StylePseudoClassPressed}), new[]
|
||||
{
|
||||
new StyleProperty(PanelContainer.StylePropertyPanel, buttonRectPressed),
|
||||
}),
|
||||
new StyleRule(new SelectorElement(typeof(ActionSlot), null, null, new[] {ContainerButton.StylePseudoClassDisabled}), new[]
|
||||
{
|
||||
new StyleProperty(PanelContainer.StylePropertyPanel, buttonRectDisabled),
|
||||
}),
|
||||
|
||||
// action menu item buttons
|
||||
new StyleRule(new SelectorElement(typeof(ActionMenuItem), null, null, new[] {ContainerButton.StylePseudoClassNormal}), new[]
|
||||
{
|
||||
new StyleProperty(ContainerButton.StylePropertyStyleBox, buttonRectActionMenuItem),
|
||||
}),
|
||||
// we don't actually disable the action menu items, only change their style based on the underlying action being revoked
|
||||
new StyleRule(new SelectorElement(typeof(ActionMenuItem), new [] {StyleClassActionMenuItemRevoked}, null, new[] {ContainerButton.StylePseudoClassNormal}), new[]
|
||||
{
|
||||
new StyleProperty(ContainerButton.StylePropertyStyleBox, buttonRectActionMenuItemRevoked),
|
||||
}),
|
||||
new StyleRule(new SelectorElement(typeof(ActionMenuItem), null, null, new[] {ContainerButton.StylePseudoClassHover}), new[]
|
||||
{
|
||||
new StyleProperty(ContainerButton.StylePropertyStyleBox, buttonRectActionMenuItemHover),
|
||||
}),
|
||||
new StyleRule(new SelectorElement(typeof(ActionMenuItem), null, null, new[] {ContainerButton.StylePseudoClassPressed}), new[]
|
||||
{
|
||||
new StyleProperty(ContainerButton.StylePropertyStyleBox, buttonRectActionMenuItemPressed),
|
||||
}),
|
||||
|
||||
// Main menu: Make those buttons bigger.
|
||||
new StyleRule(new SelectorChild(
|
||||
new SelectorElement(typeof(Button), null, "mainMenu", null),
|
||||
@@ -413,6 +531,13 @@ namespace Content.Client.UserInterface.Stylesheets
|
||||
new StyleProperty("font-color", Color.Gray),
|
||||
}),
|
||||
|
||||
// Action searchbox lineedit
|
||||
new StyleRule(new SelectorElement(typeof(LineEdit), new[] {StyleClassActionSearchBox}, null, null),
|
||||
new[]
|
||||
{
|
||||
new StyleProperty(LineEdit.StylePropertyStyleBox, actionSearchBox),
|
||||
}),
|
||||
|
||||
// TabContainer
|
||||
new StyleRule(new SelectorElement(typeof(TabContainer), null, null, null),
|
||||
new[]
|
||||
@@ -531,6 +656,30 @@ namespace Content.Client.UserInterface.Stylesheets
|
||||
new StyleProperty("font", notoSans16)
|
||||
}),
|
||||
|
||||
// action tooltip
|
||||
new StyleRule(new SelectorElement(typeof(RichTextLabel), new[] {StyleClassTooltipActionTitle}, null, null), new[]
|
||||
{
|
||||
new StyleProperty("font", notoSansBold16)
|
||||
}),
|
||||
new StyleRule(new SelectorElement(typeof(RichTextLabel), new[] {StyleClassTooltipActionDescription}, null, null), new[]
|
||||
{
|
||||
new StyleProperty("font", notoSans15)
|
||||
}),
|
||||
new StyleRule(new SelectorElement(typeof(RichTextLabel), new[] {StyleClassTooltipActionCooldown}, null, null), new[]
|
||||
{
|
||||
new StyleProperty("font", notoSans15)
|
||||
}),
|
||||
new StyleRule(new SelectorElement(typeof(RichTextLabel), new[] {StyleClassTooltipActionRequirements}, null, null), new[]
|
||||
{
|
||||
new StyleProperty("font", notoSans15)
|
||||
}),
|
||||
|
||||
// hotbar slot
|
||||
new StyleRule(new SelectorElement(typeof(RichTextLabel), new[] {StyleClassHotbarSlotNumber}, null, null), new[]
|
||||
{
|
||||
new StyleProperty("font", notoSansDisplayBold16)
|
||||
}),
|
||||
|
||||
// Entity tooltip
|
||||
new StyleRule(
|
||||
new SelectorElement(typeof(PanelContainer), new[] {ExamineSystem.StyleClassEntityTooltip}, null,
|
||||
|
||||
@@ -74,6 +74,7 @@ Smart equip from belt: [color=#a4885c]{25}[/color]
|
||||
Open inventory: [color=#a4885c]{7}[/color]
|
||||
Open character window: [color=#a4885c]{8}[/color]
|
||||
Open crafting window: [color=#a4885c]{9}[/color]
|
||||
Open action menu: [color=#a4885c]{33}[/color]
|
||||
Focus chat: [color=#a4885c]{10}[/color]
|
||||
Focus OOC: [color=#a4885c]{26}[/color]
|
||||
Focus Admin Chat: [color=#a4885c]{27}[/color]
|
||||
@@ -94,7 +95,18 @@ Toggle debug overlay: [color=#a4885c]{18}[/color]
|
||||
Toggle entity spawner: [color=#a4885c]{19}[/color]
|
||||
Toggle tile spawner: [color=#a4885c]{20}[/color]
|
||||
Toggle sandbox window: [color=#a4885c]{21}[/color]
|
||||
Toggle admin menu [color=#a4885c]{31}[/color]",
|
||||
Toggle admin menu [color=#a4885c]{31}[/color]
|
||||
Hotbar slot 1: [color=#a4885c]{34}[/color]
|
||||
Hotbar slot 2: [color=#a4885c]{35}[/color]
|
||||
Hotbar slot 3: [color=#a4885c]{36}[/color]
|
||||
Hotbar slot 4: [color=#a4885c]{37}[/color]
|
||||
Hotbar slot 5: [color=#a4885c]{38}[/color]
|
||||
Hotbar slot 6: [color=#a4885c]{39}[/color]
|
||||
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]
|
||||
",
|
||||
Key(MoveUp), Key(MoveLeft), Key(MoveDown), Key(MoveRight),
|
||||
Key(SwapHands),
|
||||
Key(ActivateItemInHand),
|
||||
@@ -124,7 +136,18 @@ Toggle admin menu [color=#a4885c]{31}[/color]",
|
||||
Key(TryPullObject),
|
||||
Key(MovePulledObject),
|
||||
Key(OpenAdminMenu),
|
||||
Key(ReleasePulledObject)));
|
||||
Key(ReleasePulledObject),
|
||||
Key(OpenActionsMenu),
|
||||
Key(Hotbar1),
|
||||
Key(Hotbar2),
|
||||
Key(Hotbar3),
|
||||
Key(Hotbar4),
|
||||
Key(Hotbar5),
|
||||
Key(Hotbar6),
|
||||
Key(Hotbar7),
|
||||
Key(Hotbar8),
|
||||
Key(Hotbar9),
|
||||
Key(Hotbar0)));
|
||||
|
||||
//Gameplay
|
||||
VBox.AddChild(new Label { FontOverride = headerFont, Text = "\nGameplay" });
|
||||
|
||||
172
Content.Client/Utility/DragDropHelper.cs
Normal file
172
Content.Client/Utility/DragDropHelper.cs
Normal file
@@ -0,0 +1,172 @@
|
||||
using Robust.Client.Interfaces.Input;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Content.Client.Utility
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper for implementing drag and drop interactions.
|
||||
///
|
||||
/// The basic flow for a drag drop interaction as per this helper is:
|
||||
/// 1. User presses mouse down on something (using class should communicate this to helper by calling MouseDown()).
|
||||
/// 2. User continues to hold the mouse down and moves the mouse outside of the defined
|
||||
/// deadzone. OnBeginDrag is invoked to see if a drag should be initiated. If so, initiates a drag.
|
||||
/// If user didn't move the mouse beyond the deadzone the drag is not initiated (OnEndDrag invoked).
|
||||
/// 3. Every Update/FrameUpdate, OnContinueDrag is invoked.
|
||||
/// 4. User lifts mouse up. This is not handled by DragDropHelper. The using class of the helper should
|
||||
/// do whatever they want and then end the drag by calling EndDrag() (which invokes OnEndDrag).
|
||||
///
|
||||
/// If for any reason the drag is ended, OnEndDrag is invoked.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">thing being dragged and dropped</typeparam>
|
||||
public class DragDropHelper<T>
|
||||
{
|
||||
private const float DefaultDragDeadzone = 2f;
|
||||
|
||||
private readonly IInputManager _inputManager;
|
||||
|
||||
private readonly OnBeginDrag _onBeginDrag;
|
||||
private readonly OnEndDrag _onEndDrag;
|
||||
private readonly OnContinueDrag _onContinueDrag;
|
||||
private readonly float _deadzone;
|
||||
|
||||
/// <summary>
|
||||
/// Convenience method, current mouse screen position as provided by inputmanager.
|
||||
/// </summary>
|
||||
public Vector2 MouseScreenPosition => _inputManager.MouseScreenPosition;
|
||||
|
||||
/// <summary>
|
||||
/// True if initiated a drag and currently dragging something.
|
||||
/// I.e. this will be false if we've just had a mousedown over something but the mouse
|
||||
/// has not moved outside of the drag deadzone.
|
||||
/// </summary>
|
||||
public bool IsDragging => _state == DragState.Dragging;
|
||||
|
||||
/// <summary>
|
||||
/// Current thing being dragged or which mouse button is being held down on.
|
||||
/// </summary>
|
||||
public T Dragged { get; private set; }
|
||||
|
||||
// screen pos where the mouse down began for the drag
|
||||
private Vector2 _mouseDownScreenPos;
|
||||
private DragState _state = DragState.NotDragging;
|
||||
|
||||
private enum DragState : byte
|
||||
{
|
||||
NotDragging,
|
||||
// not dragging yet, waiting to see
|
||||
// if they hold for long enough
|
||||
MouseDown,
|
||||
// currently dragging something
|
||||
Dragging,
|
||||
}
|
||||
|
||||
/// <param name="onBeginDrag"><see cref="OnBeginDrag"/></param>
|
||||
/// <param name="onContinueDrag"><see cref="OnContinueDrag"/></param>
|
||||
/// <param name="onEndDrag"><see cref="OnEndDrag"/></param>
|
||||
/// <param name="deadzone">drag will be triggered when mouse leaves
|
||||
/// this deadzone around the mousedown position</param>
|
||||
public DragDropHelper(OnBeginDrag onBeginDrag, OnContinueDrag onContinueDrag,
|
||||
OnEndDrag onEndDrag, float deadzone = DefaultDragDeadzone)
|
||||
{
|
||||
_deadzone = deadzone;
|
||||
_inputManager = IoCManager.Resolve<IInputManager>();
|
||||
_onBeginDrag = onBeginDrag;
|
||||
_onEndDrag = onEndDrag;
|
||||
_onContinueDrag = onContinueDrag;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tell the helper that the mouse button was pressed down on
|
||||
/// a target, thus a drag has the possibility to begin for this target.
|
||||
/// Assumes current mouse screen position is the location the mouse was clicked.
|
||||
///
|
||||
/// EndDrag should be called when the drag is done.
|
||||
/// </summary>
|
||||
public void MouseDown(T target)
|
||||
{
|
||||
if (_state != DragState.NotDragging)
|
||||
{
|
||||
EndDrag();
|
||||
}
|
||||
|
||||
Dragged = target;
|
||||
_state = DragState.MouseDown;
|
||||
_mouseDownScreenPos = _inputManager.MouseScreenPosition;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stop the current drag / drop operation no matter what state it is in.
|
||||
/// </summary>
|
||||
public void EndDrag()
|
||||
{
|
||||
Dragged = default;
|
||||
_state = DragState.NotDragging;
|
||||
_onEndDrag.Invoke();
|
||||
}
|
||||
|
||||
private void StartDragging()
|
||||
{
|
||||
if (_onBeginDrag.Invoke())
|
||||
{
|
||||
_state = DragState.Dragging;
|
||||
}
|
||||
else
|
||||
{
|
||||
EndDrag();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Should be invoked by using class every FrameUpdate or Update.
|
||||
/// </summary>
|
||||
public void Update(float frameTime)
|
||||
{
|
||||
switch (_state)
|
||||
{
|
||||
// check if dragging should begin
|
||||
case DragState.MouseDown:
|
||||
{
|
||||
var screenPos = _inputManager.MouseScreenPosition;
|
||||
if ((_mouseDownScreenPos - screenPos).Length > _deadzone)
|
||||
{
|
||||
StartDragging();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case DragState.Dragging:
|
||||
{
|
||||
if (!_onContinueDrag.Invoke(frameTime))
|
||||
{
|
||||
EndDrag();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when a drag is confirmed and going to be initiated. Implementation should
|
||||
/// typically set the drag shadow texture based on the target.
|
||||
/// </summary>
|
||||
/// <returns>true if drag should begin, false to end.</returns>
|
||||
public delegate bool OnBeginDrag();
|
||||
|
||||
/// <summary>
|
||||
/// Invoked every frame when drag is ongoing. Typically implementation should
|
||||
/// make the drag shadow follow the mouse position.
|
||||
/// </summary>
|
||||
/// <returns>true if drag should continue, false to end.</returns>
|
||||
public delegate bool OnContinueDrag(float frameTime);
|
||||
|
||||
/// <summary>
|
||||
/// invoked when
|
||||
/// the drag drop is ending for any reason. This
|
||||
/// should typically just clear the drag shadow.
|
||||
/// </summary>
|
||||
public delegate void OnEndDrag();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
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.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;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.GameObjects.Components.Mobs
|
||||
{
|
||||
[TestFixture]
|
||||
[TestOf(typeof(SharedActionsComponent))]
|
||||
[TestOf(typeof(ClientActionsComponent))]
|
||||
[TestOf(typeof(ServerActionsComponent))]
|
||||
public class ActionsComponentTests : ContentIntegrationTest
|
||||
{
|
||||
[Test]
|
||||
public async Task GrantsAndRevokesActionsTest()
|
||||
{
|
||||
var (client, server) = await StartConnectedServerClientPair();
|
||||
|
||||
await server.WaitIdleAsync();
|
||||
await client.WaitIdleAsync();
|
||||
|
||||
var serverPlayerManager = server.ResolveDependency<Robust.Server.Interfaces.Player.IPlayerManager>();
|
||||
var innateActions = new List<ActionType>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var player = serverPlayerManager.GetAllPlayers().Single();
|
||||
var playerEnt = player.AttachedEntity;
|
||||
var actionsComponent = playerEnt.GetComponent<ServerActionsComponent>();
|
||||
|
||||
// player should begin with their innate actions granted
|
||||
innateActions.AddRange(actionsComponent.InnateActions);
|
||||
foreach (var innateAction in actionsComponent.InnateActions)
|
||||
{
|
||||
Assert.That(actionsComponent.TryGetActionState(innateAction, out var innateState));
|
||||
Assert.That(innateState.Enabled);
|
||||
}
|
||||
|
||||
actionsComponent.Grant(ActionType.DebugInstant);
|
||||
Assert.That(actionsComponent.TryGetActionState(ActionType.HumanScream, out var state) && state.Enabled);
|
||||
});
|
||||
|
||||
// check that client has the actions
|
||||
await server.WaitRunTicks(5);
|
||||
await client.WaitRunTicks(5);
|
||||
|
||||
var clientPlayerMgr = client.ResolveDependency<IPlayerManager>();
|
||||
var clientUIMgr = client.ResolveDependency<IUserInterfaceManager>();
|
||||
var expectedOrder = new List<ActionType>();
|
||||
await client.WaitAssertion(() =>
|
||||
{
|
||||
|
||||
var local = clientPlayerMgr.LocalPlayer;
|
||||
var controlled = local.ControlledEntity;
|
||||
var actionsComponent = controlled.GetComponent<ClientActionsComponent>();
|
||||
|
||||
// we should have our innate actions and debug1.
|
||||
foreach (var innateAction in innateActions)
|
||||
{
|
||||
Assert.That(actionsComponent.TryGetActionState(innateAction, out var innateState));
|
||||
Assert.That(innateState.Enabled);
|
||||
}
|
||||
Assert.That(actionsComponent.TryGetActionState(ActionType.DebugInstant, out var state) && state.Enabled);
|
||||
|
||||
// innate actions should've auto-populated into our slots (in non-deterministic order),
|
||||
// but debug1 should be in the last slot
|
||||
var actionsUI =
|
||||
clientUIMgr.StateRoot.Children.FirstOrDefault(c => c is ActionsUI) as ActionsUI;
|
||||
Assert.That(actionsUI, Is.Not.Null);
|
||||
|
||||
var expectedInnate = new HashSet<ActionType>(innateActions);
|
||||
var expectEmpty = false;
|
||||
expectedOrder.Clear();
|
||||
foreach (var slot in actionsUI.Slots)
|
||||
{
|
||||
if (expectEmpty)
|
||||
{
|
||||
Assert.That(slot.HasAssignment, Is.False);
|
||||
Assert.That(slot.Item, Is.Null);
|
||||
Assert.That(slot.Action, Is.Null);
|
||||
Assert.That(slot.ActionEnabled, Is.False);
|
||||
continue;
|
||||
}
|
||||
Assert.That(slot.HasAssignment);
|
||||
// all the actions we gave so far are not tied to an item
|
||||
Assert.That(slot.Item, Is.Null);
|
||||
Assert.That(slot.Action, Is.Not.Null);
|
||||
Assert.That(slot.ActionEnabled);
|
||||
var asAction = slot.Action as ActionPrototype;
|
||||
Assert.That(asAction, Is.Not.Null);
|
||||
expectedOrder.Add(asAction.ActionType);
|
||||
|
||||
if (expectedInnate.Count != 0)
|
||||
{
|
||||
Assert.That(expectedInnate.Remove(asAction.ActionType));
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.That(asAction.ActionType, Is.EqualTo(ActionType.DebugInstant));
|
||||
Assert.That(slot.Cooldown, Is.Null);
|
||||
expectEmpty = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// now revoke the action and check that the client sees it as revoked
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var player = serverPlayerManager.GetAllPlayers().Single();
|
||||
var playerEnt = player.AttachedEntity;
|
||||
var actionsComponent = playerEnt.GetComponent<ServerActionsComponent>();
|
||||
actionsComponent.Revoke(ActionType.DebugInstant);
|
||||
});
|
||||
|
||||
await server.WaitRunTicks(5);
|
||||
await client.WaitRunTicks(5);
|
||||
|
||||
await client.WaitAssertion(() =>
|
||||
{
|
||||
|
||||
var local = clientPlayerMgr.LocalPlayer;
|
||||
var controlled = local.ControlledEntity;
|
||||
var actionsComponent = controlled.GetComponent<ClientActionsComponent>();
|
||||
|
||||
// we should have our innate actions, but debug1 should be revoked
|
||||
foreach (var innateAction in innateActions)
|
||||
{
|
||||
Assert.That(actionsComponent.TryGetActionState(innateAction, out var innateState));
|
||||
Assert.That(innateState.Enabled);
|
||||
}
|
||||
Assert.That(actionsComponent.TryGetActionState(ActionType.DebugInstant, out var state), Is.False);
|
||||
|
||||
// all actions should be in the same order as before, but the slot with DebugInstant should appear
|
||||
// disabled.
|
||||
var actionsUI =
|
||||
clientUIMgr.StateRoot.Children.FirstOrDefault(c => c is ActionsUI) as ActionsUI;
|
||||
Assert.That(actionsUI, Is.Not.Null);
|
||||
|
||||
var idx = 0;
|
||||
foreach (var slot in actionsUI.Slots)
|
||||
{
|
||||
if (idx < expectedOrder.Count)
|
||||
{
|
||||
var expected = expectedOrder[idx++];
|
||||
Assert.That(slot.HasAssignment);
|
||||
// all the actions we gave so far are not tied to an item
|
||||
Assert.That(slot.Item, Is.Null);
|
||||
Assert.That(slot.Action, Is.Not.Null);
|
||||
var asAction = slot.Action as ActionPrototype;
|
||||
Assert.That(asAction, Is.Not.Null);
|
||||
|
||||
if (asAction.ActionType == ActionType.DebugInstant)
|
||||
{
|
||||
Assert.That(slot.ActionEnabled, Is.False);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.That(slot.ActionEnabled);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.That(slot.HasAssignment, Is.False);
|
||||
Assert.That(slot.Item, Is.Null);
|
||||
Assert.That(slot.Action, Is.Null);
|
||||
Assert.That(slot.ActionEnabled, Is.False);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,12 @@
|
||||
using System.Threading.Tasks;
|
||||
using Content.Client.GameObjects.Components.Mobs;
|
||||
using Content.Client.UserInterface;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Server.GameObjects.Components.Mobs;
|
||||
using Content.Shared.Alert;
|
||||
using NUnit.Framework;
|
||||
using Robust.Client.Interfaces.UserInterface;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.Interfaces.Map;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.GameObjects.Components.Mobs
|
||||
{
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace Content.IntegrationTests.Tests.Gravity
|
||||
name: HumanDummy
|
||||
id: HumanDummy
|
||||
components:
|
||||
- type: AlertsUI
|
||||
- type: Alerts
|
||||
";
|
||||
[Test]
|
||||
public async Task WeightlessStatusTest()
|
||||
|
||||
39
Content.Server/Actions/DebugInstant.cs
Normal file
39
Content.Server/Actions/DebugInstant.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using Content.Server.Utility;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Utility;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Server.Actions
|
||||
{
|
||||
/// <summary>
|
||||
/// Just shows a popup message.asd
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public class DebugInstant : IInstantAction, IInstantItemAction
|
||||
{
|
||||
public string Message { get; private set; }
|
||||
public float Cooldown { get; private set; }
|
||||
|
||||
public void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
serializer.DataField(this, x => x.Message, "message", "Instant action used.");
|
||||
serializer.DataField(this, x => x.Cooldown, "cooldown", 0);
|
||||
}
|
||||
|
||||
public void DoInstantAction(InstantItemActionEventArgs args)
|
||||
{
|
||||
args.Performer.PopupMessageEveryone(Message);
|
||||
if (Cooldown > 0)
|
||||
{
|
||||
args.ItemActions.Cooldown(args.ActionType, Cooldowns.SecondsFromNow(Cooldown));
|
||||
}
|
||||
}
|
||||
|
||||
public void DoInstantAction(InstantActionEventArgs args)
|
||||
{
|
||||
args.Performer.PopupMessageEveryone(Message);
|
||||
args.PerformerActions.Cooldown(args.ActionType, Cooldowns.SecondsFromNow(Cooldown));
|
||||
}
|
||||
}
|
||||
}
|
||||
28
Content.Server/Actions/DebugTargetEntity.cs
Normal file
28
Content.Server/Actions/DebugTargetEntity.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using Content.Server.Utility;
|
||||
using Content.Shared.Actions;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Server.Actions
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public class DebugTargetEntity : ITargetEntityAction, ITargetEntityItemAction
|
||||
{
|
||||
|
||||
public void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
}
|
||||
|
||||
public void DoTargetEntityAction(TargetEntityItemActionEventArgs args)
|
||||
{
|
||||
args.Performer.PopupMessageEveryone(args.Item.Name + ": Clicked " +
|
||||
args.Target.Name);
|
||||
}
|
||||
|
||||
public void DoTargetEntityAction(TargetEntityActionEventArgs args)
|
||||
{
|
||||
args.Performer.PopupMessageEveryone("Clicked " +
|
||||
args.Target.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
29
Content.Server/Actions/DebugTargetPoint.cs
Normal file
29
Content.Server/Actions/DebugTargetPoint.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using Content.Server.Utility;
|
||||
using Content.Shared.Actions;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Server.Actions
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public class DebugTargetPoint : ITargetPointAction, ITargetPointItemAction
|
||||
{
|
||||
public void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
}
|
||||
|
||||
public void DoTargetPointAction(TargetPointItemActionEventArgs args)
|
||||
{
|
||||
args.Performer.PopupMessageEveryone(args.Item.Name + ": Clicked local position " +
|
||||
args.Target);
|
||||
}
|
||||
|
||||
public void DoTargetPointAction(TargetPointActionEventArgs args)
|
||||
{
|
||||
args.Performer.PopupMessageEveryone("Clicked local position " +
|
||||
args.Target);
|
||||
}
|
||||
}
|
||||
}
|
||||
48
Content.Server/Actions/DebugToggle.cs
Normal file
48
Content.Server/Actions/DebugToggle.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using Content.Server.Utility;
|
||||
using Content.Shared.Actions;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Server.Actions
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public class DebugToggle : IToggleAction, IToggleItemAction
|
||||
{
|
||||
public string MessageOn { get; private set; }
|
||||
public string MessageOff { get; private set; }
|
||||
|
||||
public void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
serializer.DataField(this, x => x.MessageOn, "messageOn", "on!");
|
||||
serializer.DataField(this, x => x.MessageOff, "messageOff", "off!");
|
||||
}
|
||||
|
||||
public bool DoToggleAction(ToggleItemActionEventArgs args)
|
||||
{
|
||||
if (args.ToggledOn)
|
||||
{
|
||||
args.Performer.PopupMessageEveryone(args.Item.Name + ": " + MessageOn);
|
||||
}
|
||||
else
|
||||
{
|
||||
args.Performer.PopupMessageEveryone(args.Item.Name + ": " +MessageOff);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool DoToggleAction(ToggleActionEventArgs args)
|
||||
{
|
||||
if (args.ToggledOn)
|
||||
{
|
||||
args.Performer.PopupMessageEveryone(MessageOn);
|
||||
}
|
||||
else
|
||||
{
|
||||
args.Performer.PopupMessageEveryone(MessageOff);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
82
Content.Server/Actions/ScreamAction.cs
Normal file
82
Content.Server/Actions/ScreamAction.cs
Normal file
@@ -0,0 +1,82 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.GameObjects.Components.Mobs;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Audio;
|
||||
using Content.Shared.GameObjects.Components.Mobs;
|
||||
using Content.Shared.GameObjects.EntitySystems;
|
||||
using Content.Shared.Preferences;
|
||||
using Content.Shared.Utility;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.GameObjects.EntitySystems;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameObjects.Systems;
|
||||
using Robust.Shared.Interfaces.Random;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Server.Actions
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public class ScreamAction : IInstantAction
|
||||
{
|
||||
private const float Variation = 0.125f;
|
||||
private const float Volume = 4f;
|
||||
|
||||
private List<string> _male;
|
||||
private List<string> _female;
|
||||
private string _wilhelm;
|
||||
/// seconds
|
||||
private float _cooldown;
|
||||
|
||||
private IRobustRandom _random;
|
||||
|
||||
public ScreamAction()
|
||||
{
|
||||
_random = IoCManager.Resolve<IRobustRandom>();
|
||||
}
|
||||
|
||||
public void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
serializer.DataField(ref _male, "male", null);
|
||||
serializer.DataField(ref _female, "female", null);
|
||||
serializer.DataField(ref _wilhelm, "wilhelm", null);
|
||||
serializer.DataField(ref _cooldown, "cooldown", 10);
|
||||
}
|
||||
|
||||
public void DoInstantAction(InstantActionEventArgs args)
|
||||
{
|
||||
if (!ActionBlockerSystem.CanSpeak(args.Performer)) return;
|
||||
if (!args.Performer.TryGetComponent<HumanoidAppearanceComponent>(out var humanoid)) return;
|
||||
if (!args.Performer.TryGetComponent<SharedActionsComponent>(out var actions)) return;
|
||||
|
||||
if (_random.Prob(.01f) && !string.IsNullOrWhiteSpace(_wilhelm))
|
||||
{
|
||||
EntitySystem.Get<AudioSystem>().PlayFromEntity(_wilhelm, args.Performer, AudioParams.Default.WithVolume(Volume));
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (humanoid.Sex)
|
||||
{
|
||||
case Sex.Male:
|
||||
if (_male == null) break;
|
||||
EntitySystem.Get<AudioSystem>().PlayFromEntity(_random.Pick(_male), args.Performer,
|
||||
AudioHelpers.WithVariation(Variation).WithVolume(Volume));
|
||||
break;
|
||||
case Sex.Female:
|
||||
if (_female == null) break;
|
||||
EntitySystem.Get<AudioSystem>().PlayFromEntity(_random.Pick(_female), args.Performer,
|
||||
AudioHelpers.WithVariation(Variation).WithVolume(Volume));
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
actions.Cooldown(args.ActionType, Cooldowns.SecondsFromNow(_cooldown));
|
||||
}
|
||||
}
|
||||
}
|
||||
24
Content.Server/Alert/Click/ResistFire.cs
Normal file
24
Content.Server/Alert/Click/ResistFire.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using Content.Server.GameObjects.Components.Atmos;
|
||||
using Content.Shared.Alert;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Server.Alert.Click
|
||||
{
|
||||
/// <summary>
|
||||
/// Resist fire
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public class ResistFire : IAlertClick
|
||||
{
|
||||
public void ExposeData(ObjectSerializer serializer) { }
|
||||
|
||||
public void AlertClicked(ClickAlertEventArgs args)
|
||||
{
|
||||
if (args.Player.TryGetComponent(out FlammableComponent flammable))
|
||||
{
|
||||
flammable.Resist();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
24
Content.Server/Alert/Click/StopPiloting.cs
Normal file
24
Content.Server/Alert/Click/StopPiloting.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using Content.Server.GameObjects.Components.Movement;
|
||||
using Content.Shared.Alert;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Server.Alert.Click
|
||||
{
|
||||
/// <summary>
|
||||
/// Stop piloting shuttle
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public class StopPiloting : IAlertClick
|
||||
{
|
||||
public void ExposeData(ObjectSerializer serializer) { }
|
||||
|
||||
public void AlertClicked(ClickAlertEventArgs args)
|
||||
{
|
||||
if (args.Player.TryGetComponent(out ShuttleControllerComponent controller))
|
||||
{
|
||||
controller.RemoveController();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
27
Content.Server/Alert/Click/StopPulling.cs
Normal file
27
Content.Server/Alert/Click/StopPulling.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using Content.Shared.Alert;
|
||||
using Content.Shared.GameObjects.Components.Pulling;
|
||||
using Content.Shared.GameObjects.EntitySystems;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.GameObjects.Systems;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Server.Alert.Click
|
||||
{
|
||||
/// <summary>
|
||||
/// Stop pulling something
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public class StopPulling : IAlertClick
|
||||
{
|
||||
public void ExposeData(ObjectSerializer serializer) { }
|
||||
|
||||
public void AlertClicked(ClickAlertEventArgs args)
|
||||
{
|
||||
EntitySystem
|
||||
.Get<SharedPullingSystem>()
|
||||
.GetPulled(args.Player)?
|
||||
.GetComponentOrNull<SharedPullableComponent>()?
|
||||
.TryStopPull();
|
||||
}
|
||||
}
|
||||
}
|
||||
24
Content.Server/Alert/Click/Unbuckle.cs
Normal file
24
Content.Server/Alert/Click/Unbuckle.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using Content.Server.GameObjects.Components.Buckle;
|
||||
using Content.Shared.Alert;
|
||||
using Robust.Shared.Serialization;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Content.Server.Alert.Click
|
||||
{
|
||||
/// <summary>
|
||||
/// Unbuckles if player is currently buckled.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public class Unbuckle : IAlertClick
|
||||
{
|
||||
public void ExposeData(ObjectSerializer serializer) { }
|
||||
|
||||
public void AlertClicked(ClickAlertEventArgs args)
|
||||
{
|
||||
if (args.Player.TryGetComponent(out BuckleComponent buckle))
|
||||
{
|
||||
buckle.TryUnbuckle(args.Player);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
65
Content.Server/Commands/Actions/CooldownAction.cs
Normal file
65
Content.Server/Commands/Actions/CooldownAction.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using Content.Server.Administration;
|
||||
using Content.Server.GameObjects.Components.Mobs;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Administration;
|
||||
using Robust.Server.Interfaces.Console;
|
||||
using Robust.Server.Interfaces.Player;
|
||||
using Robust.Shared.Interfaces.Timing;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
namespace Content.Server.Commands.Actions
|
||||
{
|
||||
[AdminCommand(AdminFlags.Debug)]
|
||||
public sealed class CooldownAction : IClientCommand
|
||||
{
|
||||
public string Command => "coolaction";
|
||||
public string Description => "Sets a cooldown on an action for a player, defaulting to current player";
|
||||
public string Help => "coolaction <actionType> <seconds> <name or userID, omit for current player>";
|
||||
|
||||
public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args)
|
||||
{
|
||||
if (player == null) return;
|
||||
var attachedEntity = player.AttachedEntity;
|
||||
if (args.Length > 2)
|
||||
{
|
||||
var target = args[2];
|
||||
if (!CommandUtils.TryGetAttachedEntityByUsernameOrId(shell, target, player, out attachedEntity)) return;
|
||||
}
|
||||
|
||||
if (attachedEntity == null) return;
|
||||
if (!attachedEntity.TryGetComponent(out ServerActionsComponent? actionsComponent))
|
||||
{
|
||||
shell.SendText(player, "user has no actions component");
|
||||
return;
|
||||
}
|
||||
|
||||
var actionTypeRaw = args[0];
|
||||
if (!Enum.TryParse<ActionType>(actionTypeRaw, out var actionType))
|
||||
{
|
||||
shell.SendText(player, "unrecognized ActionType enum value, please" +
|
||||
" ensure you used correct casing: " + actionTypeRaw);
|
||||
return;
|
||||
}
|
||||
var actionMgr = IoCManager.Resolve<ActionManager>();
|
||||
|
||||
if (!actionMgr.TryGet(actionType, out var action))
|
||||
{
|
||||
shell.SendText(player, "unrecognized actionType " + actionType);
|
||||
return;
|
||||
}
|
||||
|
||||
var cooldownStart = IoCManager.Resolve<IGameTiming>().CurTime;
|
||||
if (!uint.TryParse(args[1], out var seconds))
|
||||
{
|
||||
shell.SendText(player, "cannot parse seconds: " + args[1]);
|
||||
return;
|
||||
}
|
||||
|
||||
var cooldownEnd = cooldownStart.Add(TimeSpan.FromSeconds(seconds));
|
||||
|
||||
actionsComponent.Cooldown(action.ActionType, (cooldownStart, cooldownEnd));
|
||||
}
|
||||
}
|
||||
}
|
||||
52
Content.Server/Commands/Actions/GrantAction.cs
Normal file
52
Content.Server/Commands/Actions/GrantAction.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using Content.Server.Administration;
|
||||
using Content.Server.GameObjects.Components.Mobs;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Administration;
|
||||
using Robust.Server.Interfaces.Console;
|
||||
using Robust.Server.Interfaces.Player;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
namespace Content.Server.Commands.Actions
|
||||
{
|
||||
[AdminCommand(AdminFlags.Debug)]
|
||||
public sealed class GrantAction : IClientCommand
|
||||
{
|
||||
public string Command => "grantaction";
|
||||
public string Description => "Grants an action to a player, defaulting to current player";
|
||||
public string Help => "grantaction <actionType> <name or userID, omit for current player>";
|
||||
public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args)
|
||||
{
|
||||
if (player == null) return;
|
||||
var attachedEntity = player.AttachedEntity;
|
||||
if (args.Length > 1)
|
||||
{
|
||||
var target = args[1];
|
||||
if (!Commands.CommandUtils.TryGetAttachedEntityByUsernameOrId(shell, target, player, out attachedEntity)) return;
|
||||
}
|
||||
|
||||
if (attachedEntity == null) return;
|
||||
if (!attachedEntity.TryGetComponent(out ServerActionsComponent? actionsComponent))
|
||||
{
|
||||
shell.SendText(player, "user has no actions component");
|
||||
return;
|
||||
}
|
||||
|
||||
var actionTypeRaw = args[0];
|
||||
if (!Enum.TryParse<ActionType>(actionTypeRaw, out var actionType))
|
||||
{
|
||||
shell.SendText(player, "unrecognized ActionType enum value, please" +
|
||||
" ensure you used correct casing: " + actionTypeRaw);
|
||||
return;
|
||||
}
|
||||
var actionMgr = IoCManager.Resolve<ActionManager>();
|
||||
if (!actionMgr.TryGet(actionType, out var action))
|
||||
{
|
||||
shell.SendText(player, "unrecognized actionType " + actionType);
|
||||
return;
|
||||
}
|
||||
actionsComponent.Grant(action.ActionType);
|
||||
}
|
||||
}
|
||||
}
|
||||
53
Content.Server/Commands/Actions/RevokeAction.cs
Normal file
53
Content.Server/Commands/Actions/RevokeAction.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using Content.Server.Administration;
|
||||
using Content.Server.GameObjects.Components.Mobs;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Administration;
|
||||
using Robust.Server.Interfaces.Console;
|
||||
using Robust.Server.Interfaces.Player;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
namespace Content.Server.Commands.Actions
|
||||
{
|
||||
[AdminCommand(AdminFlags.Debug)]
|
||||
public sealed class RevokeAction : IClientCommand
|
||||
{
|
||||
public string Command => "revokeaction";
|
||||
public string Description => "Revokes an action from a player, defaulting to current player";
|
||||
public string Help => "revokeaction <actionType> <name or userID, omit for current player>";
|
||||
|
||||
public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args)
|
||||
{
|
||||
if (player == null) return;
|
||||
var attachedEntity = player.AttachedEntity;
|
||||
if (args.Length > 1)
|
||||
{
|
||||
var target = args[1];
|
||||
if (!CommandUtils.TryGetAttachedEntityByUsernameOrId(shell, target, player, out attachedEntity)) return;
|
||||
}
|
||||
if (attachedEntity == null) return;
|
||||
if (!attachedEntity.TryGetComponent(out ServerActionsComponent? actionsComponent))
|
||||
{
|
||||
shell.SendText(player, "user has no actions component");
|
||||
return;
|
||||
}
|
||||
|
||||
var actionTypeRaw = args[0];
|
||||
if (!Enum.TryParse<ActionType>(actionTypeRaw, out var actionType))
|
||||
{
|
||||
shell.SendText(player, "unrecognized ActionType enum value, please" +
|
||||
" ensure you used correct casing: " + actionTypeRaw);
|
||||
return;
|
||||
}
|
||||
var actionMgr = IoCManager.Resolve<ActionManager>();
|
||||
if (!actionMgr.TryGet(actionType, out var action))
|
||||
{
|
||||
shell.SendText(player, "unrecognized actionType " + actionType);
|
||||
return;
|
||||
}
|
||||
|
||||
actionsComponent.Revoke(action.ActionType);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,22 +19,20 @@ namespace Content.Server.Commands.Alerts
|
||||
|
||||
public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args)
|
||||
{
|
||||
var attachedEntity = player?.AttachedEntity;
|
||||
|
||||
if (attachedEntity == null)
|
||||
if (player?.AttachedEntity == null)
|
||||
{
|
||||
shell.SendText(player, "You don't have an entity.");
|
||||
return;
|
||||
}
|
||||
|
||||
var attachedEntity = player.AttachedEntity;
|
||||
|
||||
if (args.Length > 1)
|
||||
{
|
||||
var target = args[1];
|
||||
if (!CommandUtils.TryGetAttachedEntityByUsernameOrId(shell, target, player, out attachedEntity)) return;
|
||||
}
|
||||
|
||||
if (!CommandUtils.ValidateAttachedEntity(shell, player, attachedEntity)) return;
|
||||
|
||||
if (!attachedEntity.TryGetComponent(out ServerAlertsComponent? alertsComponent))
|
||||
{
|
||||
shell.SendText(player, "user has no alerts component");
|
||||
|
||||
@@ -39,9 +39,6 @@ namespace Content.Server.Commands.Alerts
|
||||
if (!CommandUtils.TryGetAttachedEntityByUsernameOrId(shell, target, player, out attachedEntity)) return;
|
||||
}
|
||||
|
||||
if (!CommandUtils.ValidateAttachedEntity(shell, player, attachedEntity))
|
||||
return;
|
||||
|
||||
if (!attachedEntity.TryGetComponent(out ServerAlertsComponent? alertsComponent))
|
||||
{
|
||||
shell.SendText(player, "user has no alerts component");
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Robust.Server.Interfaces.Console;
|
||||
using Robust.Server.Interfaces.Player;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
@@ -17,7 +19,7 @@ namespace Content.Server.Commands
|
||||
/// sending a failure to the performer if unable to.
|
||||
/// </summary>
|
||||
public static bool TryGetSessionByUsernameOrId(IConsoleShell shell,
|
||||
string usernameOrId, IPlayerSession performer, out IPlayerSession session)
|
||||
string usernameOrId, IPlayerSession performer, [NotNullWhen(true)] out IPlayerSession? session)
|
||||
{
|
||||
var plyMgr = IoCManager.Resolve<IPlayerManager>();
|
||||
if (plyMgr.TryGetSessionByUsername(usernameOrId, out session)) return true;
|
||||
@@ -37,7 +39,7 @@ namespace Content.Server.Commands
|
||||
/// sending a failure to the performer if unable to.
|
||||
/// </summary>
|
||||
public static bool TryGetAttachedEntityByUsernameOrId(IConsoleShell shell,
|
||||
string usernameOrId, IPlayerSession performer, out IEntity attachedEntity)
|
||||
string usernameOrId, IPlayerSession performer, [NotNullWhen(true)] out IEntity? attachedEntity)
|
||||
{
|
||||
attachedEntity = null;
|
||||
if (!TryGetSessionByUsernameOrId(shell, usernameOrId, performer, out var session)) return false;
|
||||
@@ -50,17 +52,5 @@ namespace Content.Server.Commands
|
||||
attachedEntity = session.AttachedEntity;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if attached entity is null, returning false and sending a message
|
||||
/// to performer if not.
|
||||
/// </summary>
|
||||
public static bool ValidateAttachedEntity(IConsoleShell shell, IPlayerSession performer, IEntity attachedEntity)
|
||||
{
|
||||
if (attachedEntity != null) return true;
|
||||
shell.SendText(performer, "User has no attached entity.");
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ using Content.Server.Interfaces.Chat;
|
||||
using Content.Server.Interfaces.GameTicking;
|
||||
using Content.Server.Interfaces.PDA;
|
||||
using Content.Server.Sandbox;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Kitchen;
|
||||
using Content.Shared.Alert;
|
||||
using Robust.Server.Interfaces.Player;
|
||||
@@ -81,6 +82,7 @@ namespace Content.Server
|
||||
_gameTicker.Initialize();
|
||||
IoCManager.Resolve<RecipeManager>().Initialize();
|
||||
IoCManager.Resolve<AlertManager>().Initialize();
|
||||
IoCManager.Resolve<ActionManager>().Initialize();
|
||||
IoCManager.Resolve<BlackboardManager>().Initialize();
|
||||
IoCManager.Resolve<ConsiderationsManager>().Initialize();
|
||||
IoCManager.Resolve<IPDAUplinkManager>().Initialize();
|
||||
|
||||
@@ -100,7 +100,7 @@ namespace Content.Server.GameObjects.Components.Atmos
|
||||
return;
|
||||
}
|
||||
|
||||
status?.ShowAlert(AlertType.Fire, onClickAlert: OnClickAlert);
|
||||
status?.ShowAlert(AlertType.Fire);
|
||||
|
||||
if (FireStacks > 0)
|
||||
{
|
||||
@@ -152,14 +152,6 @@ namespace Content.Server.GameObjects.Components.Atmos
|
||||
}
|
||||
}
|
||||
|
||||
private void OnClickAlert(ClickAlertEventArgs args)
|
||||
{
|
||||
if (args.Player.TryGetComponent(out FlammableComponent flammable))
|
||||
{
|
||||
flammable.Resist();
|
||||
}
|
||||
}
|
||||
|
||||
public void CollideWith(IEntity collidedWith)
|
||||
{
|
||||
if (!collidedWith.TryGetComponent(out FlammableComponent otherFlammable))
|
||||
|
||||
@@ -5,18 +5,22 @@ using Content.Server.Explosions;
|
||||
using Content.Server.GameObjects.Components.Body.Respiratory;
|
||||
using Content.Server.Interfaces;
|
||||
using Content.Server.Utility;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Audio;
|
||||
using Content.Shared.GameObjects.Components.Atmos.GasTank;
|
||||
using Content.Shared.GameObjects.Components.Mobs;
|
||||
using Content.Shared.GameObjects.EntitySystems;
|
||||
using Content.Shared.GameObjects.Verbs;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.GameObjects.Components.UserInterface;
|
||||
using Robust.Server.GameObjects.EntitySystems;
|
||||
using Robust.Server.Interfaces.GameObjects;
|
||||
using Robust.Server.Interfaces.Player;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameObjects.ComponentDependencies;
|
||||
using Robust.Shared.GameObjects.Systems;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Localization;
|
||||
@@ -30,13 +34,15 @@ namespace Content.Server.GameObjects.Components.Atmos
|
||||
[ComponentReference(typeof(IActivate))]
|
||||
public class GasTankComponent : SharedGasTankComponent, IExamine, IGasMixtureHolder, IUse, IDropped, IActivate
|
||||
{
|
||||
private const float MaxExplosionRange = 14f;
|
||||
private const float MaxExplosionRange = 14f;
|
||||
private const float DefaultOutputPressure = Atmospherics.OneAtmosphere;
|
||||
|
||||
private float _pressureResistance;
|
||||
|
||||
private int _integrity = 3;
|
||||
|
||||
[ComponentDependency] private readonly ItemActionsComponent? _itemActions = null;
|
||||
|
||||
[ViewVariables] private BoundUserInterface? _userInterface;
|
||||
|
||||
[ViewVariables] public GasMixture? Air { get; set; }
|
||||
@@ -191,14 +197,18 @@ namespace Content.Server.GameObjects.Components.Atmos
|
||||
|
||||
private void UpdateUserInterface(bool initialUpdate = false)
|
||||
{
|
||||
var internals = GetInternalsComponent();
|
||||
_userInterface?.SetState(
|
||||
new GasTankBoundUserInterfaceState
|
||||
{
|
||||
TankPressure = Air?.Pressure ?? 0,
|
||||
OutputPressure = initialUpdate ? OutputPressure : (float?) null,
|
||||
InternalsConnected = IsConnected,
|
||||
CanConnectInternals = IsFunctional && GetInternalsComponent() != null
|
||||
CanConnectInternals = IsFunctional && internals != null
|
||||
});
|
||||
|
||||
if (internals == null) return;
|
||||
_itemActions?.GrantOrUpdate(ItemActionType.ToggleInternals, IsFunctional, IsConnected);
|
||||
}
|
||||
|
||||
private void UserInterfaceOnOnReceiveMessage(ServerBoundUserInterfaceMessage message)
|
||||
@@ -214,8 +224,9 @@ namespace Content.Server.GameObjects.Components.Atmos
|
||||
}
|
||||
}
|
||||
|
||||
private void ToggleInternals()
|
||||
internal void ToggleInternals()
|
||||
{
|
||||
if (!ActionBlockerSystem.CanUse(GetInternalsComponent()?.Owner)) return;
|
||||
if (IsConnected)
|
||||
{
|
||||
DisconnectFromInternals();
|
||||
@@ -311,6 +322,11 @@ namespace Content.Server.GameObjects.Components.Atmos
|
||||
_integrity++;
|
||||
}
|
||||
|
||||
public void Dropped(DroppedEventArgs eventArgs)
|
||||
{
|
||||
DisconnectFromInternals(eventArgs.User);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Open interaction window
|
||||
/// </summary>
|
||||
@@ -341,10 +357,21 @@ namespace Content.Server.GameObjects.Components.Atmos
|
||||
component.OpenInterface(actor.playerSession);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dropped(DroppedEventArgs eventArgs)
|
||||
[UsedImplicitly]
|
||||
public class ToggleInternalsAction : IToggleItemAction
|
||||
{
|
||||
public void ExposeData(ObjectSerializer serializer) {}
|
||||
|
||||
public bool DoToggleAction(ToggleItemActionEventArgs args)
|
||||
{
|
||||
DisconnectFromInternals(eventArgs.User);
|
||||
if (!args.Item.TryGetComponent<GasTankComponent>(out var gasTankComponent)) return false;
|
||||
// no change
|
||||
if (gasTankComponent.IsConnected == args.ToggledOn) return false;
|
||||
gasTankComponent.ToggleInternals();
|
||||
// did we successfully toggle to the desired status?
|
||||
return gasTankComponent.IsConnected == args.ToggledOn;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,8 +108,7 @@ namespace Content.Server.GameObjects.Components.Buckle
|
||||
|
||||
if (Buckled)
|
||||
{
|
||||
_serverAlertsComponent.ShowAlert(BuckledTo != null ? BuckledTo.BuckledAlertType : AlertType.Buckled,
|
||||
onClickAlert: OnClickAlert);
|
||||
_serverAlertsComponent.ShowAlert(BuckledTo?.BuckledAlertType ?? AlertType.Buckled);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -117,14 +116,6 @@ namespace Content.Server.GameObjects.Components.Buckle
|
||||
}
|
||||
}
|
||||
|
||||
private void OnClickAlert(ClickAlertEventArgs args)
|
||||
{
|
||||
if (args.Player.TryGetComponent(out BuckleComponent? buckle))
|
||||
{
|
||||
buckle.TryUnbuckle(args.Player);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Reattaches this entity to the strap, modifying its position and rotation.
|
||||
|
||||
@@ -30,6 +30,7 @@ namespace Content.Server.GameObjects.Components.GUI
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(IHandsComponent))]
|
||||
[ComponentReference(typeof(ISharedHandsComponent))]
|
||||
[ComponentReference(typeof(SharedHandsComponent))]
|
||||
public class HandsComponent : SharedHandsComponent, IHandsComponent, IBodyPartAdded, IBodyPartRemoved
|
||||
{
|
||||
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
|
||||
@@ -82,7 +83,7 @@ namespace Content.Server.GameObjects.Components.GUI
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsHolding(IEntity entity)
|
||||
public override bool IsHolding(IEntity entity)
|
||||
{
|
||||
foreach (var hand in _hands)
|
||||
{
|
||||
@@ -165,6 +166,7 @@ namespace Content.Server.GameObjects.Components.GUI
|
||||
}
|
||||
|
||||
Dirty();
|
||||
|
||||
var success = hand.Container.Insert(item.Owner);
|
||||
if (success)
|
||||
{
|
||||
@@ -172,6 +174,9 @@ namespace Content.Server.GameObjects.Components.GUI
|
||||
OnItemChanged?.Invoke();
|
||||
}
|
||||
|
||||
_entitySystemManager.GetEntitySystem<InteractionSystem>().EquippedHandInteraction(Owner, item.Owner,
|
||||
ToSharedHand(hand));
|
||||
|
||||
_entitySystemManager.GetEntitySystem<InteractionSystem>().HandSelectedInteraction(Owner, item.Owner);
|
||||
|
||||
return success;
|
||||
@@ -266,6 +271,9 @@ namespace Content.Server.GameObjects.Components.GUI
|
||||
return false;
|
||||
}
|
||||
|
||||
_entitySystemManager.GetEntitySystem<InteractionSystem>().UnequippedHandInteraction(Owner, item.Owner,
|
||||
ToSharedHand(hand));
|
||||
|
||||
if (doDropInteraction && !DroppedInteraction(item, false))
|
||||
return false;
|
||||
|
||||
@@ -288,6 +296,61 @@ namespace Content.Server.GameObjects.Components.GUI
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public bool Drop(string slot, BaseContainer targetContainer, bool doMobChecks = true, bool doDropInteraction = true)
|
||||
{
|
||||
if (slot == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(slot));
|
||||
}
|
||||
|
||||
if (targetContainer == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(targetContainer));
|
||||
}
|
||||
|
||||
var hand = GetHand(slot);
|
||||
if (!CanDrop(slot, doMobChecks) || hand?.Entity == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!hand.Container.CanRemove(hand.Entity))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!targetContainer.CanInsert(hand.Entity))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var item = hand.Entity.GetComponent<ItemComponent>();
|
||||
|
||||
if (!hand.Container.Remove(hand.Entity))
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
_entitySystemManager.GetEntitySystem<InteractionSystem>().UnequippedHandInteraction(Owner, item.Owner,
|
||||
ToSharedHand(hand));
|
||||
|
||||
if (doDropInteraction && !DroppedInteraction(item, doMobChecks))
|
||||
return false;
|
||||
|
||||
item.RemovedFromSlot();
|
||||
|
||||
if (!targetContainer.Insert(item.Owner))
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
OnItemChanged?.Invoke();
|
||||
|
||||
Dirty();
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Drop(IEntity entity, EntityCoordinates coords, bool doMobChecks = true, bool doDropInteraction = true)
|
||||
{
|
||||
if (entity == null)
|
||||
@@ -323,57 +386,6 @@ namespace Content.Server.GameObjects.Components.GUI
|
||||
return Drop(slot, Owner.Transform.Coordinates, mobChecks, doDropInteraction);
|
||||
}
|
||||
|
||||
public bool Drop(string slot, BaseContainer targetContainer, bool doMobChecks = true, bool doDropInteraction = true)
|
||||
{
|
||||
if (slot == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(slot));
|
||||
}
|
||||
|
||||
if (targetContainer == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(targetContainer));
|
||||
}
|
||||
|
||||
var hand = GetHand(slot);
|
||||
if (!CanDrop(slot, doMobChecks) || hand?.Entity == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!hand.Container.CanRemove(hand.Entity))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!targetContainer.CanInsert(hand.Entity))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var item = hand.Entity.GetComponent<ItemComponent>();
|
||||
|
||||
if (!hand.Container.Remove(hand.Entity))
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
if (doDropInteraction && !DroppedInteraction(item, doMobChecks))
|
||||
return false;
|
||||
|
||||
item.RemovedFromSlot();
|
||||
|
||||
if (!targetContainer.Insert(item.Owner))
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
OnItemChanged?.Invoke();
|
||||
|
||||
Dirty();
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Drop(IEntity entity, BaseContainer targetContainer, bool doMobChecks = true, bool doDropInteraction = true)
|
||||
{
|
||||
if (entity == null)
|
||||
@@ -463,19 +475,28 @@ namespace Content.Server.GameObjects.Components.GUI
|
||||
|
||||
for (var i = 0; i < _hands.Count; i++)
|
||||
{
|
||||
var location = i == 0
|
||||
? HandLocation.Right
|
||||
: i == _hands.Count - 1
|
||||
? HandLocation.Left
|
||||
: HandLocation.Middle;
|
||||
|
||||
var hand = _hands[i].ToShared(i, location);
|
||||
var hand = _hands[i].ToShared(i, IndexToHandLocation(i));
|
||||
hands[i] = hand;
|
||||
}
|
||||
|
||||
return new HandsComponentState(hands, ActiveHand);
|
||||
}
|
||||
|
||||
private HandLocation IndexToHandLocation(int index)
|
||||
{
|
||||
return index == 0
|
||||
? HandLocation.Right
|
||||
: index == _hands.Count - 1
|
||||
? HandLocation.Left
|
||||
: HandLocation.Middle;
|
||||
}
|
||||
|
||||
private SharedHand ToSharedHand(Hand hand)
|
||||
{
|
||||
var index = _hands.IndexOf(hand);
|
||||
return hand.ToShared(index, IndexToHandLocation(index));
|
||||
}
|
||||
|
||||
public void SwapHands()
|
||||
{
|
||||
if (ActiveHand == null)
|
||||
|
||||
@@ -25,6 +25,7 @@ using static Content.Shared.GameObjects.Components.Inventory.SharedInventoryComp
|
||||
namespace Content.Server.GameObjects.Components.GUI
|
||||
{
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(SharedInventoryComponent))]
|
||||
public class InventoryComponent : SharedInventoryComponent, IExAct, IEffectBlocker, IPressureProtection
|
||||
{
|
||||
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
|
||||
@@ -572,5 +573,20 @@ namespace Content.Server.GameObjects.Components.GUI
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override bool IsEquipped(IEntity item)
|
||||
{
|
||||
if (item == null) return false;
|
||||
foreach (var containerSlot in _slotContainers.Values)
|
||||
{
|
||||
// we don't want a recursive check here
|
||||
if (containerSlot.Contains(item))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
#nullable enable
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.GameObjects.Components.Atmos;
|
||||
using Content.Server.GameObjects.Components.GUI;
|
||||
using Content.Server.GameObjects.Components.Items.Clothing;
|
||||
using Content.Server.GameObjects.Components.Items.Storage;
|
||||
using Content.Server.GameObjects.Components.Mobs;
|
||||
using Content.Server.GameObjects.Components.Power;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Server.GameObjects.Components.Weapon.Ranged.Barrels;
|
||||
using Content.Shared.GameObjects.Components;
|
||||
using Content.Shared.GameObjects.Components.Mobs;
|
||||
using Content.Shared.GameObjects.EntitySystems;
|
||||
using Content.Shared.GameObjects.Verbs;
|
||||
using Content.Shared.Interfaces;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Content.Shared.Utility;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.GameObjects.EntitySystems;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameObjects.ComponentDependencies;
|
||||
using Robust.Shared.GameObjects.Systems;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Localization;
|
||||
@@ -45,6 +53,8 @@ namespace Content.Server.GameObjects.Components.Interactable
|
||||
[ViewVariables(VVAccess.ReadWrite)] public string? TurnOnFailSound;
|
||||
[ViewVariables(VVAccess.ReadWrite)] public string? TurnOffSound;
|
||||
|
||||
[ComponentDependency] private readonly ItemActionsComponent? _itemActions = null;
|
||||
|
||||
/// <summary>
|
||||
/// Client-side ItemStatus level
|
||||
/// </summary>
|
||||
@@ -98,8 +108,9 @@ namespace Content.Server.GameObjects.Components.Interactable
|
||||
/// Illuminates the light if it is not active, extinguishes it if it is active.
|
||||
/// </summary>
|
||||
/// <returns>True if the light's status was toggled, false otherwise.</returns>
|
||||
private bool ToggleStatus(IEntity user)
|
||||
public bool ToggleStatus(IEntity user)
|
||||
{
|
||||
if (!ActionBlockerSystem.CanUse(user)) return false;
|
||||
return Activated ? TurnOff() : TurnOn(user);
|
||||
}
|
||||
|
||||
@@ -112,6 +123,7 @@ namespace Content.Server.GameObjects.Components.Interactable
|
||||
|
||||
SetState(false);
|
||||
Activated = false;
|
||||
UpdateLightAction();
|
||||
|
||||
if (makeNoise)
|
||||
{
|
||||
@@ -132,6 +144,7 @@ namespace Content.Server.GameObjects.Components.Interactable
|
||||
{
|
||||
if (TurnOnFailSound != null) EntitySystem.Get<AudioSystem>().PlayFromEntity(TurnOnFailSound, Owner);
|
||||
Owner.PopupMessage(user, Loc.GetString("Cell missing..."));
|
||||
UpdateLightAction();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -142,10 +155,12 @@ namespace Content.Server.GameObjects.Components.Interactable
|
||||
{
|
||||
if (TurnOnFailSound != null) EntitySystem.Get<AudioSystem>().PlayFromEntity(TurnOnFailSound, Owner);
|
||||
Owner.PopupMessage(user, Loc.GetString("Dead cell..."));
|
||||
UpdateLightAction();
|
||||
return false;
|
||||
}
|
||||
|
||||
Activated = true;
|
||||
UpdateLightAction();
|
||||
SetState(true);
|
||||
|
||||
if (TurnOnSound != null) EntitySystem.Get<AudioSystem>().PlayFromEntity(TurnOnSound, Owner);
|
||||
@@ -175,6 +190,11 @@ namespace Content.Server.GameObjects.Components.Interactable
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateLightAction()
|
||||
{
|
||||
_itemActions?.Toggle(ItemActionType.ToggleLight, Activated);
|
||||
}
|
||||
|
||||
public void OnUpdate(float frameTime)
|
||||
{
|
||||
if (Cell == null)
|
||||
@@ -249,4 +269,17 @@ namespace Content.Server.GameObjects.Components.Interactable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
public class ToggleLightAction : IToggleItemAction
|
||||
{
|
||||
public void ExposeData(ObjectSerializer serializer) {}
|
||||
|
||||
public bool DoToggleAction(ToggleItemActionEventArgs args)
|
||||
{
|
||||
if (!args.Item.TryGetComponent<HandheldLightComponent>(out var lightComponent)) return false;
|
||||
if (lightComponent.Activated == args.ToggledOn) return false;
|
||||
return lightComponent.ToggleStatus(args.Performer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
using Content.Shared.Interfaces;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Items
|
||||
{
|
||||
/// <summary>
|
||||
/// Pops up a message when equipped / unequipped (including hands).
|
||||
/// For debugging purposes.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public class DebugEquipComponent : Component, IEquipped, IEquippedHand, IUnequipped, IUnequippedHand
|
||||
{
|
||||
public override string Name => "DebugEquip";
|
||||
public void Equipped(EquippedEventArgs eventArgs)
|
||||
{
|
||||
eventArgs.User.PopupMessage("equipped " + Owner.Name);
|
||||
}
|
||||
|
||||
public void EquippedHand(EquippedHandEventArgs eventArgs)
|
||||
{
|
||||
eventArgs.User.PopupMessage("equipped hand " + Owner.Name);
|
||||
}
|
||||
|
||||
public void Unequipped(UnequippedEventArgs eventArgs)
|
||||
{
|
||||
eventArgs.User.PopupMessage("unequipped " + Owner.Name);
|
||||
}
|
||||
|
||||
public void UnequippedHand(UnequippedHandEventArgs eventArgs)
|
||||
{
|
||||
eventArgs.User.PopupMessage("unequipped hand" + Owner.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.GameObjects.Components.Mobs;
|
||||
using Content.Shared.GameObjects.EntitySystems;
|
||||
using Robust.Server.Interfaces.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Interfaces.Network;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Players;
|
||||
|
||||
namespace Content.Server.GameObjects.Components.Mobs
|
||||
{
|
||||
[RegisterComponent]
|
||||
[ComponentReference(typeof(SharedActionsComponent))]
|
||||
public sealed class ServerActionsComponent : SharedActionsComponent
|
||||
{
|
||||
[Dependency] private readonly IServerEntityManager _entityManager = default!;
|
||||
|
||||
public override void HandleNetworkMessage(ComponentMessage message, INetChannel netChannel, ICommonSession? session = null)
|
||||
{
|
||||
base.HandleNetworkMessage(message, netChannel, session);
|
||||
|
||||
if (message is not BasePerformActionMessage performActionMessage) return;
|
||||
if (session == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(session));
|
||||
}
|
||||
|
||||
var player = session.AttachedEntity;
|
||||
if (player != Owner) return;
|
||||
var attempt = ActionAttempt(performActionMessage, session);
|
||||
if (attempt == null) return;
|
||||
|
||||
if (!attempt.TryGetActionState(this, out var actionState) || !actionState.Enabled)
|
||||
{
|
||||
Logger.DebugS("action", "user {0} attempted to use" +
|
||||
" action {1} which is not granted to them", player.Name,
|
||||
attempt);
|
||||
return;
|
||||
}
|
||||
|
||||
if (actionState.IsOnCooldown(GameTiming))
|
||||
{
|
||||
Logger.DebugS("action", "user {0} attempted to use" +
|
||||
" action {1} which is on cooldown", player.Name,
|
||||
attempt);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (performActionMessage.BehaviorType)
|
||||
{
|
||||
case BehaviorType.Instant:
|
||||
attempt.DoInstantAction(player);
|
||||
break;
|
||||
case BehaviorType.Toggle:
|
||||
if (performActionMessage is not IToggleActionMessage toggleMsg) return;
|
||||
if (toggleMsg.ToggleOn == actionState.ToggledOn)
|
||||
{
|
||||
Logger.DebugS("action", "user {0} attempted to" +
|
||||
" toggle action {1} to {2}, but it is already toggled {2}", player.Name,
|
||||
attempt.Action.Name, toggleMsg.ToggleOn);
|
||||
return;
|
||||
}
|
||||
|
||||
if (attempt.DoToggleAction(player, toggleMsg.ToggleOn))
|
||||
{
|
||||
attempt.ToggleAction(this, toggleMsg.ToggleOn);
|
||||
}
|
||||
else
|
||||
{
|
||||
// if client predicted the toggle will work, need to reset
|
||||
// that prediction
|
||||
Dirty();
|
||||
}
|
||||
break;
|
||||
case BehaviorType.TargetPoint:
|
||||
if (performActionMessage is not ITargetPointActionMessage targetPointMsg) return;
|
||||
if (!CheckRangeAndSetFacing(targetPointMsg.Target, player)) return;
|
||||
attempt.DoTargetPointAction(player, targetPointMsg.Target);
|
||||
break;
|
||||
case BehaviorType.TargetEntity:
|
||||
if (performActionMessage is not ITargetEntityActionMessage targetEntityMsg) return;
|
||||
if (!EntityManager.TryGetEntity(targetEntityMsg.Target, out var entity))
|
||||
{
|
||||
Logger.DebugS("action", "user {0} attempted to" +
|
||||
" perform target entity action {1} but could not find entity with " +
|
||||
"provided uid {2}", player.Name, attempt.Action.Name,
|
||||
targetEntityMsg.Target);
|
||||
return;
|
||||
}
|
||||
if (!CheckRangeAndSetFacing(entity.Transform.Coordinates, player)) return;
|
||||
|
||||
attempt.DoTargetEntityAction(player, entity);
|
||||
break;
|
||||
case BehaviorType.None:
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
|
||||
private IActionAttempt? ActionAttempt(BasePerformActionMessage message, ICommonSession session)
|
||||
{
|
||||
IActionAttempt? attempt;
|
||||
switch (message)
|
||||
{
|
||||
case PerformActionMessage performActionMessage:
|
||||
if (!ActionManager.TryGet(performActionMessage.ActionType, out var action))
|
||||
{
|
||||
Logger.DebugS("action", "user {0} attempted to perform" +
|
||||
" unrecognized action {1}", session.AttachedEntity,
|
||||
performActionMessage.ActionType);
|
||||
return null;
|
||||
}
|
||||
attempt = new ActionAttempt(action);
|
||||
break;
|
||||
case PerformItemActionMessage performItemActionMessage:
|
||||
if (!ActionManager.TryGet(performItemActionMessage.ActionType, out var itemAction))
|
||||
{
|
||||
Logger.DebugS("action", "user {0} attempted to perform" +
|
||||
" unrecognized item action {1}",
|
||||
session.AttachedEntity, performItemActionMessage.ActionType);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!EntityManager.TryGetEntity(performItemActionMessage.Item, out var item))
|
||||
{
|
||||
Logger.DebugS("action", "user {0} attempted to perform" +
|
||||
" item action {1} for unknown item {2}",
|
||||
session.AttachedEntity, performItemActionMessage.ActionType, performItemActionMessage.Item);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!item.TryGetComponent<ItemActionsComponent>(out var actionsComponent))
|
||||
{
|
||||
Logger.DebugS("action", "user {0} attempted to perform" +
|
||||
" item action {1} for item {2} which has no ItemActionsComponent",
|
||||
session.AttachedEntity, performItemActionMessage.ActionType, item);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (actionsComponent.Holder != session.AttachedEntity)
|
||||
{
|
||||
Logger.DebugS("action", "user {0} attempted to perform" +
|
||||
" item action {1} for item {2} which they are not holding",
|
||||
session.AttachedEntity, performItemActionMessage.ActionType, item);
|
||||
return null;
|
||||
}
|
||||
|
||||
attempt = new ItemActionAttempt(itemAction, item, actionsComponent);
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
if (message.BehaviorType != attempt.Action.BehaviorType)
|
||||
{
|
||||
Logger.DebugS("action", "user {0} attempted to" +
|
||||
" perform action {1} as a {2} behavior, but this action is actually a" +
|
||||
" {3} behavior", session.AttachedEntity, attempt, message.BehaviorType,
|
||||
attempt.Action.BehaviorType);
|
||||
return null;
|
||||
}
|
||||
|
||||
return attempt;
|
||||
}
|
||||
|
||||
private bool CheckRangeAndSetFacing(EntityCoordinates target, IEntity player)
|
||||
{
|
||||
// ensure it's within their clickable range
|
||||
var targetWorldPos = target.ToMapPos(EntityManager);
|
||||
var rangeBox = new Box2(player.Transform.WorldPosition, player.Transform.WorldPosition)
|
||||
.Enlarged(_entityManager.MaxUpdateRange);
|
||||
if (!rangeBox.Contains(targetWorldPos))
|
||||
{
|
||||
Logger.DebugS("action", "user {0} attempted to" +
|
||||
" perform target action further than allowed range",
|
||||
player.Name);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ActionBlockerSystem.CanChangeDirection(player)) return true;
|
||||
|
||||
// don't set facing unless they clicked far enough away
|
||||
var diff = targetWorldPos - player.Transform.WorldPosition;
|
||||
if (diff.LengthSquared > 0.01f)
|
||||
{
|
||||
player.Transform.LocalRotation = new Angle(diff);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using Content.Server.GameObjects.EntitySystems;
|
||||
using Content.Shared.Alert;
|
||||
using Content.Shared.GameObjects.Components.Mobs;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameObjects.Systems;
|
||||
@@ -42,11 +43,6 @@ namespace Content.Server.GameObjects.Components.Mobs
|
||||
base.OnRemove();
|
||||
}
|
||||
|
||||
public override ComponentState GetComponentState()
|
||||
{
|
||||
return new AlertsComponentState(CreateAlertStatesArray());
|
||||
}
|
||||
|
||||
public override void HandleNetworkMessage(ComponentMessage message, INetChannel netChannel, ICommonSession session = null)
|
||||
{
|
||||
base.HandleNetworkMessage(message, netChannel, session);
|
||||
@@ -67,14 +63,21 @@ namespace Content.Server.GameObjects.Components.Mobs
|
||||
break;
|
||||
}
|
||||
|
||||
// TODO: Implement clicking other status effects in the HUD
|
||||
if (AlertManager.TryDecode(msg.EncodedAlert, out var alert))
|
||||
if (!IsShowingAlert(msg.AlertType))
|
||||
{
|
||||
PerformAlertClickCallback(alert, player);
|
||||
Logger.DebugS("alert", "user {0} attempted to" +
|
||||
" click alert {1} which is not currently showing for them",
|
||||
player.Name, msg.AlertType);
|
||||
break;
|
||||
}
|
||||
|
||||
if (AlertManager.TryGet(msg.AlertType, out var alert))
|
||||
{
|
||||
alert.OnClick.AlertClicked(new ClickAlertEventArgs(player, alert));
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.WarningS("alert", "unrecognized encoded alert {0}", msg.EncodedAlert);
|
||||
Logger.WarningS("alert", "unrecognized encoded alert {0}", msg.AlertType);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
@@ -145,15 +145,7 @@ namespace Content.Server.GameObjects.Components.Movement
|
||||
mind.Mind.Visit(Owner);
|
||||
_controller = entity;
|
||||
|
||||
status.ShowAlert(_pilotingAlertType, onClickAlert: OnClickAlert);
|
||||
}
|
||||
|
||||
private void OnClickAlert(ClickAlertEventArgs args)
|
||||
{
|
||||
if (args.Player.TryGetComponent(out ShuttleControllerComponent? controller))
|
||||
{
|
||||
controller.RemoveController();
|
||||
}
|
||||
status.ShowAlert(_pilotingAlertType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.GameObjects.Components.GUI;
|
||||
using Content.Server.GameObjects.Components.Items.Storage;
|
||||
using Content.Server.GameObjects.Components.Mobs;
|
||||
using Content.Server.GameObjects.Components.Pulling;
|
||||
using Content.Server.GameObjects.Components.Timing;
|
||||
using Content.Server.Interfaces.GameObjects.Components.Items;
|
||||
using Content.Shared.GameObjects.Components.Inventory;
|
||||
using Content.Shared.GameObjects.Components.Items;
|
||||
using Content.Shared.GameObjects.EntitySystemMessages;
|
||||
using Content.Shared.GameObjects.EntitySystems;
|
||||
using Content.Shared.Input;
|
||||
@@ -113,11 +115,9 @@ namespace Content.Server.GameObjects.EntitySystems.Click
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Activates the Activate behavior of an object
|
||||
/// Activates the IActivate behavior of an object
|
||||
/// Verifies that the user is capable of doing the use interaction first
|
||||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
/// <param name="used"></param>
|
||||
public void TryInteractionActivate(IEntity user, IEntity used)
|
||||
{
|
||||
if (user != null && used != null && ActionBlockerSystem.CanUse(user))
|
||||
@@ -504,7 +504,7 @@ namespace Content.Server.GameObjects.EntitySystems.Click
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Activates the Use behavior of an object
|
||||
/// Activates the IUse behaviors of an entity
|
||||
/// Verifies that the user is capable of doing the use interaction first
|
||||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
@@ -518,8 +518,8 @@ namespace Content.Server.GameObjects.EntitySystems.Click
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Activates/Uses an object in control/possession of a user
|
||||
/// If the item has the IUse interface on one of its components we use the object in our hand
|
||||
/// Activates the IUse behaviors of an entity without first checking
|
||||
/// if the user is capable of doing the use interaction.
|
||||
/// </summary>
|
||||
public void UseInteraction(IEntity user, IEntity used)
|
||||
{
|
||||
@@ -679,6 +679,48 @@ namespace Content.Server.GameObjects.EntitySystems.Click
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calls EquippedHand on all components that implement the IEquippedHand interface
|
||||
/// on an item.
|
||||
/// </summary>
|
||||
public void EquippedHandInteraction(IEntity user, IEntity item, SharedHand hand)
|
||||
{
|
||||
var equippedHandMessage = new EquippedHandMessage(user, item, hand);
|
||||
RaiseLocalEvent(equippedHandMessage);
|
||||
if (equippedHandMessage.Handled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var comps = item.GetAllComponents<IEquippedHand>().ToList();
|
||||
|
||||
foreach (var comp in comps)
|
||||
{
|
||||
comp.EquippedHand(new EquippedHandEventArgs(user, hand));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calls UnequippedHand on all components that implement the IUnequippedHand interface
|
||||
/// on an item.
|
||||
/// </summary>
|
||||
public void UnequippedHandInteraction(IEntity user, IEntity item, SharedHand hand)
|
||||
{
|
||||
var unequippedHandMessage = new UnequippedHandMessage(user, item, hand);
|
||||
RaiseLocalEvent(unequippedHandMessage);
|
||||
if (unequippedHandMessage.Handled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var comps = item.GetAllComponents<IUnequippedHand>().ToList();
|
||||
|
||||
foreach (var comp in comps)
|
||||
{
|
||||
comp.UnequippedHand(new UnequippedHandEventArgs(user, hand));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Activates the Dropped behavior of an object
|
||||
/// Verifies that the user is capable of doing the drop interaction first
|
||||
@@ -757,7 +799,6 @@ namespace Content.Server.GameObjects.EntitySystems.Click
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Will have two behaviors, either "uses" the weapon at range on the entity if it is capable of accepting that action
|
||||
/// Or it will use the weapon itself on the position clicked, regardless of what was there
|
||||
|
||||
@@ -20,6 +20,7 @@ using Content.Server.PDA;
|
||||
using Content.Server.Preferences;
|
||||
using Content.Server.Sandbox;
|
||||
using Content.Server.Utility;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Interfaces;
|
||||
using Content.Shared.Kitchen;
|
||||
using Content.Shared.Alert;
|
||||
@@ -43,6 +44,7 @@ namespace Content.Server
|
||||
IoCManager.Register<IServerDbManager, ServerDbManager>();
|
||||
IoCManager.Register<RecipeManager, RecipeManager>();
|
||||
IoCManager.Register<AlertManager, AlertManager>();
|
||||
IoCManager.Register<ActionManager, ActionManager>();
|
||||
IoCManager.Register<IPDAUplinkManager,PDAUplinkManager>();
|
||||
IoCManager.Register<INodeGroupFactory, NodeGroupFactory>();
|
||||
IoCManager.Register<INodeGroupManager, NodeGroupManager>();
|
||||
|
||||
70
Content.Shared/Actions/ActionManager.cs
Normal file
70
Content.Shared/Actions/ActionManager.cs
Normal file
@@ -0,0 +1,70 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Actions
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides access to all configured actions by action type.
|
||||
/// </summary>
|
||||
public class ActionManager
|
||||
{
|
||||
[Dependency]
|
||||
private readonly IPrototypeManager _prototypeManager = default!;
|
||||
|
||||
private Dictionary<ActionType, ActionPrototype> _typeToAction;
|
||||
private Dictionary<ItemActionType, ItemActionPrototype> _typeToItemAction;
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_typeToAction = new Dictionary<ActionType, ActionPrototype>();
|
||||
foreach (var action in _prototypeManager.EnumeratePrototypes<ActionPrototype>())
|
||||
{
|
||||
if (!_typeToAction.TryAdd(action.ActionType, action))
|
||||
{
|
||||
Logger.ErrorS("action",
|
||||
"Found action with duplicate actionType {0} - all actions must have" +
|
||||
" a unique actionType, this one will be skipped", action.ActionType);
|
||||
}
|
||||
}
|
||||
|
||||
_typeToItemAction = new Dictionary<ItemActionType, ItemActionPrototype>();
|
||||
foreach (var action in _prototypeManager.EnumeratePrototypes<ItemActionPrototype>())
|
||||
{
|
||||
if (!_typeToItemAction.TryAdd(action.ActionType, action))
|
||||
{
|
||||
Logger.ErrorS("action",
|
||||
"Found itemAction with duplicate actionType {0} - all actions must have" +
|
||||
" a unique actionType, this one will be skipped", action.ActionType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <returns>all action prototypes of all types</returns>
|
||||
public IEnumerable<BaseActionPrototype> EnumerateActions()
|
||||
{
|
||||
return _typeToAction.Values.Concat<BaseActionPrototype>(_typeToItemAction.Values);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get the action of the indicated type
|
||||
/// </summary>
|
||||
/// <returns>true if found</returns>
|
||||
public bool TryGet(ActionType actionType, out ActionPrototype action)
|
||||
{
|
||||
return _typeToAction.TryGetValue(actionType, out action);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get the item action of the indicated type
|
||||
/// </summary>
|
||||
/// <returns>true if found</returns>
|
||||
public bool TryGet(ItemActionType actionType, out ItemActionPrototype action)
|
||||
{
|
||||
return _typeToItemAction.TryGetValue(actionType, out action);
|
||||
}
|
||||
}
|
||||
}
|
||||
100
Content.Shared/Actions/ActionPrototype.cs
Normal file
100
Content.Shared/Actions/ActionPrototype.cs
Normal file
@@ -0,0 +1,100 @@
|
||||
using Content.Shared.Interfaces;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using YamlDotNet.RepresentationModel;
|
||||
using Robust.Shared.Log;
|
||||
|
||||
namespace Content.Shared.Actions
|
||||
{
|
||||
/// <summary>
|
||||
/// An action which is granted directly to an entity (such as an innate ability
|
||||
/// or skill).
|
||||
/// </summary>
|
||||
[Prototype("action")]
|
||||
public class ActionPrototype : BaseActionPrototype
|
||||
{
|
||||
/// <summary>
|
||||
/// Type of action, no 2 action prototypes should have the same one.
|
||||
/// </summary>
|
||||
public ActionType ActionType { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The IInstantAction that should be invoked when performing this
|
||||
/// action. Null if this is not an Instant ActionBehaviorType.
|
||||
/// Will be null on client side if the behavior is not in Content.Client.
|
||||
/// </summary>
|
||||
public IInstantAction InstantAction { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The IToggleAction that should be invoked when performing this
|
||||
/// action. Null if this is not a Toggle ActionBehaviorType.
|
||||
/// Will be null on client side if the behavior is not in Content.Client.
|
||||
/// </summary>
|
||||
public IToggleAction ToggleAction { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ITargetEntityAction that should be invoked when performing this
|
||||
/// action. Null if this is not a TargetEntity ActionBehaviorType.
|
||||
/// Will be null on client side if the behavior is not in Content.Client.
|
||||
/// </summary>
|
||||
public ITargetEntityAction TargetEntityAction { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ITargetPointAction that should be invoked when performing this
|
||||
/// action. Null if this is not a TargetPoint ActionBehaviorType.
|
||||
/// Will be null on client side if the behavior is not in Content.Client.
|
||||
/// </summary>
|
||||
public ITargetPointAction TargetPointAction { get; private set; }
|
||||
|
||||
public override void LoadFrom(YamlMappingNode mapping)
|
||||
{
|
||||
base.LoadFrom(mapping);
|
||||
var serializer = YamlObjectSerializer.NewReader(mapping);
|
||||
|
||||
serializer.DataField(this, x => x.ActionType, "actionType", ActionType.Error);
|
||||
if (ActionType == ActionType.Error)
|
||||
{
|
||||
Logger.ErrorS("action", "missing or invalid actionType for action with name {0}", Name);
|
||||
}
|
||||
|
||||
// TODO: Split this class into server/client after RobustToolbox#1405
|
||||
if (IoCManager.Resolve<IModuleManager>().IsClientModule) return;
|
||||
|
||||
IActionBehavior behavior = null;
|
||||
serializer.DataField(ref behavior, "behavior", null);
|
||||
switch (behavior)
|
||||
{
|
||||
case null:
|
||||
BehaviorType = BehaviorType.None;
|
||||
Logger.ErrorS("action", "missing or invalid behavior for action with name {0}", Name);
|
||||
break;
|
||||
case IInstantAction instantAction:
|
||||
ValidateBehaviorType(BehaviorType.Instant, typeof(IInstantAction));
|
||||
BehaviorType = BehaviorType.Instant;
|
||||
InstantAction = instantAction;
|
||||
break;
|
||||
case IToggleAction toggleAction:
|
||||
ValidateBehaviorType(BehaviorType.Toggle, typeof(IToggleAction));
|
||||
BehaviorType = BehaviorType.Toggle;
|
||||
ToggleAction = toggleAction;
|
||||
break;
|
||||
case ITargetEntityAction targetEntity:
|
||||
ValidateBehaviorType(BehaviorType.TargetEntity, typeof(ITargetEntityAction));
|
||||
BehaviorType = BehaviorType.TargetEntity;
|
||||
TargetEntityAction = targetEntity;
|
||||
break;
|
||||
case ITargetPointAction targetPointAction:
|
||||
ValidateBehaviorType(BehaviorType.TargetPoint, typeof(ITargetPointAction));
|
||||
BehaviorType = BehaviorType.TargetPoint;
|
||||
TargetPointAction = targetPointAction;
|
||||
break;
|
||||
default:
|
||||
BehaviorType = BehaviorType.None;
|
||||
Logger.ErrorS("action", "unrecognized behavior type for action with name {0}", Name);
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
33
Content.Shared/Actions/ActionType.cs
Normal file
33
Content.Shared/Actions/ActionType.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
namespace Content.Shared.Actions
|
||||
{
|
||||
/// <summary>
|
||||
/// Every possible action. Corresponds to actionType in action prototypes.
|
||||
/// </summary>
|
||||
public enum ActionType : byte
|
||||
{
|
||||
Error,
|
||||
HumanScream,
|
||||
DebugInstant,
|
||||
DebugToggle,
|
||||
DebugTargetPoint,
|
||||
DebugTargetPointRepeat,
|
||||
DebugTargetEntity,
|
||||
DebugTargetEntityRepeat
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Every possible item action. Corresponds to actionType in itemAction prototypes.
|
||||
/// </summary>
|
||||
public enum ItemActionType : byte
|
||||
{
|
||||
Error,
|
||||
ToggleInternals,
|
||||
ToggleLight,
|
||||
DebugInstant,
|
||||
DebugToggle,
|
||||
DebugTargetPoint,
|
||||
DebugTargetPointRepeat,
|
||||
DebugTargetEntity,
|
||||
DebugTargetEntityRepeat
|
||||
}
|
||||
}
|
||||
166
Content.Shared/Actions/BaseActionPrototype.cs
Normal file
166
Content.Shared/Actions/BaseActionPrototype.cs
Normal file
@@ -0,0 +1,166 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Shared.Interfaces;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
using YamlDotNet.RepresentationModel;
|
||||
|
||||
namespace Content.Shared.Actions
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for action prototypes.
|
||||
/// </summary>
|
||||
public abstract class BaseActionPrototype : IPrototype
|
||||
{
|
||||
/// <summary>
|
||||
/// Icon representing this action in the UI.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public SpriteSpecifier Icon { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// For toggle actions only, icon to show when toggled on. If omitted,
|
||||
/// the action will simply be highlighted when turned on.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public SpriteSpecifier IconOn { get; private set; }
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Name to show in UI. Accepts formatting.
|
||||
/// </summary>
|
||||
public FormattedMessage Name { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Description to show in UI. Accepts formatting.
|
||||
/// </summary>
|
||||
public FormattedMessage Description { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Requirements message to show in UI. Accepts formatting, but generally should be avoided
|
||||
/// so the requirements message isn't too prominent in the tooltip.
|
||||
/// </summary>
|
||||
public string Requires { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of behavior this action has. This is valid clientside and serverside.
|
||||
/// </summary>
|
||||
public BehaviorType BehaviorType { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// For targetpoint or targetentity actions, if this is true the action will remain
|
||||
/// selected after it is used, so it can be continuously re-used. If this is false,
|
||||
/// the action will be deselected after one use.
|
||||
/// </summary>
|
||||
public bool Repeat { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Filters that can be used to filter this item in action menu.
|
||||
/// </summary>
|
||||
public IEnumerable<string> Filters { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Keywords that can be used to search this item in action menu.
|
||||
/// </summary>
|
||||
public IEnumerable<string> Keywords { get; private set; }
|
||||
|
||||
public virtual void LoadFrom(YamlMappingNode mapping)
|
||||
{
|
||||
var serializer = YamlObjectSerializer.NewReader(mapping);
|
||||
|
||||
serializer.DataReadFunction("name", string.Empty,
|
||||
s => Name = FormattedMessage.FromMarkup(s));
|
||||
serializer.DataReadFunction("description", string.Empty,
|
||||
s => Description = FormattedMessage.FromMarkup(s));
|
||||
|
||||
serializer.DataField(this, x => x.Requires,"requires", null);
|
||||
serializer.DataField(this, x => x.Icon,"icon", SpriteSpecifier.Invalid);
|
||||
serializer.DataField(this, x => x.IconOn,"iconOn", SpriteSpecifier.Invalid);
|
||||
|
||||
// client needs to know what type of behavior it is even if the actual implementation is only
|
||||
// on server side. If we wanted to avoid this we'd need to always add a shared or clientside interface
|
||||
// for each action even if there was only server-side logic, which would be cumbersome
|
||||
serializer.DataField(this, x => x.BehaviorType, "behaviorType", BehaviorType.None);
|
||||
if (BehaviorType == BehaviorType.None)
|
||||
{
|
||||
Logger.ErrorS("action", "Missing behaviorType for action with name {0}", Name);
|
||||
}
|
||||
|
||||
if (BehaviorType != BehaviorType.Toggle && IconOn != SpriteSpecifier.Invalid)
|
||||
{
|
||||
Logger.ErrorS("action", "for action {0}, iconOn was specified but behavior" +
|
||||
" type was {1}. iconOn is only supported for Toggle behavior type.", Name);
|
||||
}
|
||||
|
||||
serializer.DataField(this, x => x.Repeat, "repeat", false);
|
||||
if (Repeat && BehaviorType != BehaviorType.TargetEntity && BehaviorType != BehaviorType.TargetPoint)
|
||||
{
|
||||
Logger.ErrorS("action", " action named {0} used repeat: true, but this is only supported for" +
|
||||
" TargetEntity and TargetPoint behaviorType and its behaviorType is {1}",
|
||||
Name, BehaviorType);
|
||||
}
|
||||
|
||||
serializer.DataReadFunction("filters", new List<string>(),
|
||||
rawTags =>
|
||||
{
|
||||
Filters = rawTags.Select(rawTag => rawTag.Trim()).ToList();
|
||||
});
|
||||
|
||||
serializer.DataReadFunction("keywords", new List<string>(),
|
||||
rawTags =>
|
||||
{
|
||||
Keywords = rawTags.Select(rawTag => rawTag.Trim()).ToList();
|
||||
});
|
||||
}
|
||||
|
||||
protected void ValidateBehaviorType(BehaviorType expected, Type actualInterface)
|
||||
{
|
||||
if (BehaviorType != expected)
|
||||
{
|
||||
Logger.ErrorS("action", "for action named {0}, behavior implements " +
|
||||
"{1}, so behaviorType should be {2} but was {3}", Name, actualInterface.Name, expected, BehaviorType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The behavior / logic of the action. Each of these corresponds to a particular IActionBehavior
|
||||
/// (for actions) or IItemActionBehavior (for item actions)
|
||||
/// interface. Corresponds to action.behaviorType in YAML
|
||||
/// </summary>
|
||||
public enum BehaviorType
|
||||
{
|
||||
/// <summary>
|
||||
/// Action doesn't do anything.
|
||||
/// </summary>
|
||||
None,
|
||||
|
||||
/// <summary>
|
||||
/// IInstantAction/IInstantItemAction. Action which does something immediately when used and has
|
||||
/// no target.
|
||||
/// </summary>
|
||||
Instant,
|
||||
|
||||
/// <summary>
|
||||
/// IToggleAction/IToggleItemAction Action which can be toggled on and off
|
||||
/// </summary>
|
||||
Toggle,
|
||||
|
||||
/// <summary>
|
||||
/// ITargetEntityAction/ITargetEntityItemAction. Action which is used on a targeted entity.
|
||||
/// </summary>
|
||||
TargetEntity,
|
||||
|
||||
/// <summary>
|
||||
/// ITargetPointAction/ITargetPointItemAction. Action which requires the user to select a target point, which
|
||||
/// does not necessarily have an entity on it.
|
||||
/// </summary>
|
||||
TargetPoint
|
||||
}
|
||||
}
|
||||
44
Content.Shared/Actions/IActionBehavior.cs
Normal file
44
Content.Shared/Actions/IActionBehavior.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using Content.Shared.GameObjects.Components.Mobs;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Interfaces.Serialization;
|
||||
|
||||
namespace Content.Shared.Actions
|
||||
{
|
||||
/// <summary>
|
||||
/// Currently just a marker interface delineating the different possible
|
||||
/// types of action behaviors.
|
||||
/// </summary>
|
||||
public interface IActionBehavior : IExposeData { }
|
||||
|
||||
/// <summary>
|
||||
/// Base class for all action event args
|
||||
/// </summary>
|
||||
public abstract class ActionEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Entity performing the action.
|
||||
/// </summary>
|
||||
public readonly IEntity Performer;
|
||||
/// <summary>
|
||||
/// Action being performed
|
||||
/// </summary>
|
||||
public readonly ActionType ActionType;
|
||||
/// <summary>
|
||||
/// Actions component of the performer.
|
||||
/// </summary>
|
||||
public readonly SharedActionsComponent PerformerActions;
|
||||
|
||||
public ActionEventArgs(IEntity performer, ActionType actionType)
|
||||
{
|
||||
Performer = performer;
|
||||
ActionType = actionType;
|
||||
if (!Performer.TryGetComponent(out PerformerActions))
|
||||
{
|
||||
throw new InvalidOperationException($"performer {performer.Name} tried to perform action {actionType} " +
|
||||
$" but the performer had no actions component," +
|
||||
" which should never occur");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
27
Content.Shared/Actions/IInstantAction.cs
Normal file
27
Content.Shared/Actions/IInstantAction.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Interfaces.Serialization;
|
||||
|
||||
namespace Content.Shared.Actions
|
||||
{
|
||||
/// <summary>
|
||||
/// Action which does something immediately when used and has
|
||||
/// no target.
|
||||
/// </summary>
|
||||
public interface IInstantAction : IActionBehavior
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when the instant action should be performed.
|
||||
/// Implementation should perform the server side logic of the action.
|
||||
/// </summary>
|
||||
void DoInstantAction(InstantActionEventArgs args);
|
||||
}
|
||||
|
||||
public class InstantActionEventArgs : ActionEventArgs
|
||||
{
|
||||
public InstantActionEventArgs(IEntity performer, ActionType actionType) : base(performer, actionType)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
28
Content.Shared/Actions/IInstantItemAction.cs
Normal file
28
Content.Shared/Actions/IInstantItemAction.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Interfaces.Serialization;
|
||||
|
||||
namespace Content.Shared.Actions
|
||||
{
|
||||
/// <summary>
|
||||
/// Item action which does something immediately when used and has
|
||||
/// no target.
|
||||
/// </summary>
|
||||
public interface IInstantItemAction : IItemActionBehavior
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when the instant action should be performed.
|
||||
/// Implementation should perform the server side logic of the action.
|
||||
/// </summary>
|
||||
void DoInstantAction(InstantItemActionEventArgs args);
|
||||
}
|
||||
|
||||
public class InstantItemActionEventArgs : ItemActionEventArgs
|
||||
{
|
||||
public InstantItemActionEventArgs(IEntity performer, IEntity item, ItemActionType actionType) :
|
||||
base(performer, item, actionType)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
52
Content.Shared/Actions/IItemActionBehavior.cs
Normal file
52
Content.Shared/Actions/IItemActionBehavior.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using System;
|
||||
using Content.Shared.GameObjects.Components.Mobs;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Interfaces.Serialization;
|
||||
|
||||
namespace Content.Shared.Actions
|
||||
{
|
||||
/// <summary>
|
||||
/// Currently just a marker interface delineating the different possible
|
||||
/// types of item action behaviors.
|
||||
/// </summary>
|
||||
public interface IItemActionBehavior : IExposeData
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base class for all item action event args
|
||||
/// </summary>
|
||||
public abstract class ItemActionEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Entity performing the action.
|
||||
/// </summary>
|
||||
public readonly IEntity Performer;
|
||||
/// <summary>
|
||||
/// Item being used to perform the action
|
||||
/// </summary>
|
||||
public readonly IEntity Item;
|
||||
/// <summary>
|
||||
/// Action being performed
|
||||
/// </summary>
|
||||
public readonly ItemActionType ActionType;
|
||||
/// <summary>
|
||||
/// Item actions component of the item.
|
||||
/// </summary>
|
||||
public readonly ItemActionsComponent ItemActions;
|
||||
|
||||
public ItemActionEventArgs(IEntity performer, IEntity item, ItemActionType actionType)
|
||||
{
|
||||
Performer = performer;
|
||||
ActionType = actionType;
|
||||
Item = item;
|
||||
if (!Item.TryGetComponent(out ItemActions))
|
||||
{
|
||||
throw new InvalidOperationException($"performer {performer.Name} tried to perform item action {actionType} " +
|
||||
$" for item {Item.Name} but the item had no ItemActionsComponent," +
|
||||
" which should never occur");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
33
Content.Shared/Actions/ITargetEntityAction.cs
Normal file
33
Content.Shared/Actions/ITargetEntityAction.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Interfaces.Serialization;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Shared.Actions
|
||||
{
|
||||
/// <summary>
|
||||
/// Action which is used on a targeted entity.
|
||||
/// </summary>
|
||||
public interface ITargetEntityAction : IActionBehavior
|
||||
{
|
||||
/// <summary>
|
||||
/// Invoked when the target entity action should be performed.
|
||||
/// Implementation should perform the server side logic of the action.
|
||||
/// </summary>
|
||||
void DoTargetEntityAction(TargetEntityActionEventArgs args);
|
||||
}
|
||||
|
||||
public class TargetEntityActionEventArgs : ActionEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Entity being targeted
|
||||
/// </summary>
|
||||
public readonly IEntity Target;
|
||||
|
||||
public TargetEntityActionEventArgs(IEntity performer, ActionType actionType, IEntity target) :
|
||||
base(performer, actionType)
|
||||
{
|
||||
Target = target;
|
||||
}
|
||||
}
|
||||
}
|
||||
34
Content.Shared/Actions/ITargetEntityItemAction.cs
Normal file
34
Content.Shared/Actions/ITargetEntityItemAction.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Interfaces.Serialization;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Shared.Actions
|
||||
{
|
||||
/// <summary>
|
||||
/// Item action which is used on a targeted entity.
|
||||
/// </summary>
|
||||
public interface ITargetEntityItemAction : IItemActionBehavior
|
||||
{
|
||||
/// <summary>
|
||||
/// Invoked when the target entity action should be performed.
|
||||
/// Implementation should perform the server side logic of the action.
|
||||
/// </summary>
|
||||
void DoTargetEntityAction(TargetEntityItemActionEventArgs args);
|
||||
}
|
||||
|
||||
public class TargetEntityItemActionEventArgs : ItemActionEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Entity being targeted
|
||||
/// </summary>
|
||||
public readonly IEntity Target;
|
||||
|
||||
public TargetEntityItemActionEventArgs(IEntity performer, IEntity target, IEntity item,
|
||||
ItemActionType actionType) : base(performer, item, actionType)
|
||||
{
|
||||
Target = target;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
33
Content.Shared/Actions/ITargetPointAction.cs
Normal file
33
Content.Shared/Actions/ITargetPointAction.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Shared.Actions
|
||||
{
|
||||
/// <summary>
|
||||
/// Action which requires the user to select a target point, which
|
||||
/// does not necessarily have an entity on it.
|
||||
/// </summary>
|
||||
public interface ITargetPointAction : IActionBehavior
|
||||
{
|
||||
/// <summary>
|
||||
/// Invoked when the target point action should be performed.
|
||||
/// Implementation should perform the server side logic of the action.
|
||||
/// </summary>
|
||||
void DoTargetPointAction(TargetPointActionEventArgs args);
|
||||
}
|
||||
|
||||
public class TargetPointActionEventArgs : ActionEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Local coordinates of the targeted position.
|
||||
/// </summary>
|
||||
public readonly EntityCoordinates Target;
|
||||
|
||||
public TargetPointActionEventArgs(IEntity performer, EntityCoordinates target, ActionType actionType)
|
||||
: base(performer, actionType)
|
||||
{
|
||||
Target = target;
|
||||
}
|
||||
}
|
||||
}
|
||||
33
Content.Shared/Actions/ITargetPointItemAction.cs
Normal file
33
Content.Shared/Actions/ITargetPointItemAction.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Shared.Actions
|
||||
{
|
||||
/// <summary>
|
||||
/// Item action which requires the user to select a target point, which
|
||||
/// does not necessarily have an entity on it.
|
||||
/// </summary>
|
||||
public interface ITargetPointItemAction : IItemActionBehavior
|
||||
{
|
||||
/// <summary>
|
||||
/// Invoked when the target point action should be performed.
|
||||
/// Implementation should perform the server side logic of the action.
|
||||
/// </summary>
|
||||
void DoTargetPointAction(TargetPointItemActionEventArgs args);
|
||||
}
|
||||
|
||||
public class TargetPointItemActionEventArgs : ItemActionEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Local coordinates of the targeted position.
|
||||
/// </summary>
|
||||
public readonly EntityCoordinates Target;
|
||||
|
||||
public TargetPointItemActionEventArgs(IEntity performer, EntityCoordinates target, IEntity item,
|
||||
ItemActionType actionType) : base(performer, item, actionType)
|
||||
{
|
||||
Target = target;
|
||||
}
|
||||
}
|
||||
}
|
||||
41
Content.Shared/Actions/IToggleAction.cs
Normal file
41
Content.Shared/Actions/IToggleAction.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
|
||||
namespace Content.Shared.Actions
|
||||
{
|
||||
/// <summary>
|
||||
/// Action which can be toggled on and off
|
||||
/// </summary>
|
||||
public interface IToggleAction : IActionBehavior
|
||||
{
|
||||
/// <summary>
|
||||
/// Invoked when the action will be toggled on/off.
|
||||
/// Implementation should perform the server side logic of whatever
|
||||
/// happens when it is toggled on / off.
|
||||
/// </summary>
|
||||
/// <returns>true if the attempt to toggle was successful, meaning the state should be toggled to the desired value.
|
||||
/// False to leave toggle status unchanged. This is NOT returning the new toggle status, it is only returning
|
||||
/// whether the attempt to toggle to the indicated status was successful.
|
||||
///
|
||||
/// Note that it's still okay if the implementation directly modifies toggle status via SharedActionsComponent,
|
||||
/// this is just an additional level of safety to ensure implementations will always
|
||||
/// explicitly indicate if the toggle status should be changed.</returns>
|
||||
bool DoToggleAction(ToggleActionEventArgs args);
|
||||
}
|
||||
|
||||
public class ToggleActionEventArgs : ActionEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// True if the toggle is attempting to be toggled on, false if attempting to toggle off
|
||||
/// </summary>
|
||||
public readonly bool ToggledOn;
|
||||
/// <summary>
|
||||
/// Opposite of ToggledOn
|
||||
/// </summary>
|
||||
public bool ToggledOff => !ToggledOn;
|
||||
|
||||
public ToggleActionEventArgs(IEntity performer, ActionType actionType, bool toggledOn) : base(performer, actionType)
|
||||
{
|
||||
ToggledOn = toggledOn;
|
||||
}
|
||||
}
|
||||
}
|
||||
42
Content.Shared/Actions/IToggleItemAction.cs
Normal file
42
Content.Shared/Actions/IToggleItemAction.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
|
||||
namespace Content.Shared.Actions
|
||||
{
|
||||
/// <summary>
|
||||
/// Item action which can be toggled on and off
|
||||
/// </summary>
|
||||
public interface IToggleItemAction : IItemActionBehavior
|
||||
{
|
||||
/// <summary>
|
||||
/// Invoked when the action will be toggled on/off.
|
||||
/// Implementation should perform the server side logic of whatever
|
||||
/// happens when it is toggled on / off.
|
||||
/// </summary>
|
||||
/// <returns>true if the attempt to toggle was successful, meaning the state should be toggled to the desired value.
|
||||
/// False to leave toggle status unchanged. This is NOT returning the new toggle status, it is only returning
|
||||
/// whether the attempt to toggle to the indicated status was successful.
|
||||
///
|
||||
/// Note that it's still okay if the implementation directly modifies toggle status via ItemActionsComponent,
|
||||
/// this is just an additional level of safety to ensure implementations will always
|
||||
/// explicitly indicate if the toggle status should be changed.</returns>
|
||||
bool DoToggleAction(ToggleItemActionEventArgs args);
|
||||
}
|
||||
|
||||
public class ToggleItemActionEventArgs : ItemActionEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// True if the toggle was toggled on, false if it was toggled off
|
||||
/// </summary>
|
||||
public readonly bool ToggledOn;
|
||||
/// <summary>
|
||||
/// Opposite of ToggledOn
|
||||
/// </summary>
|
||||
public bool ToggledOff => !ToggledOn;
|
||||
|
||||
public ToggleItemActionEventArgs(IEntity performer, bool toggledOn, IEntity item,
|
||||
ItemActionType actionType) : base(performer, item, actionType)
|
||||
{
|
||||
ToggledOn = toggledOn;
|
||||
}
|
||||
}
|
||||
}
|
||||
122
Content.Shared/Actions/ItemActionPrototype.cs
Normal file
122
Content.Shared/Actions/ItemActionPrototype.cs
Normal file
@@ -0,0 +1,122 @@
|
||||
using Content.Shared.Interfaces;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using YamlDotNet.RepresentationModel;
|
||||
|
||||
namespace Content.Shared.Actions
|
||||
{
|
||||
/// <summary>
|
||||
/// An action which is granted to an entity via an item (such as toggling a flashlight).
|
||||
/// </summary>
|
||||
[Prototype("itemAction")]
|
||||
public class ItemActionPrototype : BaseActionPrototype
|
||||
{
|
||||
/// <summary>
|
||||
/// Type of item action, no 2 itemAction prototypes should have the same one.
|
||||
/// </summary>
|
||||
public ItemActionType ActionType { get; private set; }
|
||||
|
||||
/// <see cref="ItemActionIconStyle"/>
|
||||
public ItemActionIconStyle IconStyle { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The IInstantItemAction that should be invoked when performing this
|
||||
/// action. Null if this is not an Instant ActionBehaviorType.
|
||||
/// Will be null on client side if the behavior is not in Content.Client.
|
||||
/// </summary>
|
||||
public IInstantItemAction InstantAction { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The IToggleItemAction that should be invoked when performing this
|
||||
/// action. Null if this is not a Toggle ActionBehaviorType.
|
||||
/// Will be null on client side if the behavior is not in Content.Client.
|
||||
/// </summary>
|
||||
public IToggleItemAction ToggleAction { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ITargetEntityItemAction that should be invoked when performing this
|
||||
/// action. Null if this is not a TargetEntity ActionBehaviorType.
|
||||
/// Will be null on client side if the behavior is not in Content.Client.
|
||||
/// </summary>
|
||||
public ITargetEntityItemAction TargetEntityAction { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ITargetPointItemAction that should be invoked when performing this
|
||||
/// action. Null if this is not a TargetPoint ActionBehaviorType.
|
||||
/// Will be null on client side if the behavior is not in Content.Client.
|
||||
/// </summary>
|
||||
public ITargetPointItemAction TargetPointAction { get; private set; }
|
||||
|
||||
public override void LoadFrom(YamlMappingNode mapping)
|
||||
{
|
||||
base.LoadFrom(mapping);
|
||||
var serializer = YamlObjectSerializer.NewReader(mapping);
|
||||
|
||||
serializer.DataField(this, x => x.ActionType, "actionType", ItemActionType.Error);
|
||||
if (ActionType == ItemActionType.Error)
|
||||
{
|
||||
Logger.ErrorS("action", "missing or invalid actionType for action with name {0}", Name);
|
||||
}
|
||||
|
||||
serializer.DataField(this, x => x.IconStyle, "iconStyle", ItemActionIconStyle.BigItem);
|
||||
|
||||
// TODO: Split this class into server/client after RobustToolbox#1405
|
||||
if (IoCManager.Resolve<IModuleManager>().IsClientModule) return;
|
||||
|
||||
IItemActionBehavior behavior = null;
|
||||
serializer.DataField(ref behavior, "behavior", null);
|
||||
switch (behavior)
|
||||
{
|
||||
case null:
|
||||
BehaviorType = BehaviorType.None;
|
||||
Logger.ErrorS("action", "missing or invalid behavior for action with name {0}", Name);
|
||||
break;
|
||||
case IInstantItemAction instantAction:
|
||||
ValidateBehaviorType(BehaviorType.Instant, typeof(IInstantItemAction));
|
||||
BehaviorType = BehaviorType.Instant;
|
||||
InstantAction = instantAction;
|
||||
break;
|
||||
case IToggleItemAction toggleAction:
|
||||
ValidateBehaviorType(BehaviorType.Toggle, typeof(IToggleItemAction));
|
||||
BehaviorType = BehaviorType.Toggle;
|
||||
ToggleAction = toggleAction;
|
||||
break;
|
||||
case ITargetEntityItemAction targetEntity:
|
||||
ValidateBehaviorType(BehaviorType.TargetEntity, typeof(ITargetEntityItemAction));
|
||||
BehaviorType = BehaviorType.TargetEntity;
|
||||
TargetEntityAction = targetEntity;
|
||||
break;
|
||||
case ITargetPointItemAction targetPointAction:
|
||||
ValidateBehaviorType(BehaviorType.TargetPoint, typeof(ITargetPointItemAction));
|
||||
BehaviorType = BehaviorType.TargetPoint;
|
||||
TargetPointAction = targetPointAction;
|
||||
break;
|
||||
default:
|
||||
BehaviorType = BehaviorType.None;
|
||||
Logger.ErrorS("action", "unrecognized behavior type for action with name {0}", Name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines how the action icon appears in the hotbar for item actions.
|
||||
/// </summary>
|
||||
public enum ItemActionIconStyle : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// The default - the item icon will be big with a small action icon in the corner
|
||||
/// </summary>
|
||||
BigItem,
|
||||
/// <summary>
|
||||
/// The action icon will be big with a small item icon in the corner
|
||||
/// </summary>
|
||||
BigAction,
|
||||
/// <summary>
|
||||
/// BigAction but no item icon will be shown in the corner.
|
||||
/// </summary>
|
||||
NoItem
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Shared.Prototypes.Kitchen;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Prototypes;
|
||||
@@ -8,41 +6,28 @@ using Robust.Shared.Prototypes;
|
||||
namespace Content.Shared.Alert
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides access to all configured alerts. Ability to encode/decode a given state
|
||||
/// to an int.
|
||||
/// Provides access to all configured alerts by alert type.
|
||||
/// </summary>
|
||||
public class AlertManager
|
||||
{
|
||||
[Dependency]
|
||||
private readonly IPrototypeManager _prototypeManager = default!;
|
||||
|
||||
private AlertPrototype[] _orderedAlerts;
|
||||
private Dictionary<AlertType, byte> _typeToIndex;
|
||||
private Dictionary<AlertType, AlertPrototype> _typeToAlert;
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
// order by type value so we can map between the id and an integer index and use
|
||||
// the index for compact alert change messages
|
||||
_orderedAlerts =
|
||||
_prototypeManager.EnumeratePrototypes<AlertPrototype>()
|
||||
.OrderBy(prototype => prototype.AlertType).ToArray();
|
||||
_typeToIndex = new Dictionary<AlertType, byte>();
|
||||
_typeToAlert = new Dictionary<AlertType, AlertPrototype>();
|
||||
|
||||
for (var i = 0; i < _orderedAlerts.Length; i++)
|
||||
foreach (var alert in _prototypeManager.EnumeratePrototypes<AlertPrototype>())
|
||||
{
|
||||
if (i > byte.MaxValue)
|
||||
{
|
||||
Logger.ErrorS("alert", "too many alerts for byte encoding ({0})! encoding will need" +
|
||||
" to be changed to use a ushort rather than byte", _typeToIndex.Count);
|
||||
break;
|
||||
}
|
||||
if (!_typeToIndex.TryAdd(_orderedAlerts[i].AlertType, (byte) i))
|
||||
if (!_typeToAlert.TryAdd(alert.AlertType, alert))
|
||||
{
|
||||
Logger.ErrorS("alert",
|
||||
"Found alert with duplicate id {0}", _orderedAlerts[i].AlertType);
|
||||
"Found alert with duplicate alertType {0} - all alerts must have" +
|
||||
" a unique alerttype, this one will be skipped", alert.AlertType);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -51,74 +36,7 @@ namespace Content.Shared.Alert
|
||||
/// <returns>true if found</returns>
|
||||
public bool TryGet(AlertType alertType, out AlertPrototype alert)
|
||||
{
|
||||
if (_typeToIndex.TryGetValue(alertType, out var idx))
|
||||
{
|
||||
alert = _orderedAlerts[idx];
|
||||
return true;
|
||||
}
|
||||
|
||||
alert = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get the alert of the indicated type along with its encoding
|
||||
/// </summary>
|
||||
/// <returns>true if found</returns>
|
||||
public bool TryGetWithEncoded(AlertType alertType, out AlertPrototype alert, out byte encoded)
|
||||
{
|
||||
if (_typeToIndex.TryGetValue(alertType, out var idx))
|
||||
{
|
||||
alert = _orderedAlerts[idx];
|
||||
encoded = (byte) idx;
|
||||
return true;
|
||||
}
|
||||
|
||||
alert = null;
|
||||
encoded = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get the compact encoded representation of this alert
|
||||
/// </summary>
|
||||
/// <returns>true if successful</returns>
|
||||
public bool TryEncode(AlertPrototype alert, out byte encoded)
|
||||
{
|
||||
return TryEncode(alert.AlertType, out encoded);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get the compact encoded representation of the alert with
|
||||
/// the indicated id
|
||||
/// </summary>
|
||||
/// <returns>true if successful</returns>
|
||||
public bool TryEncode(AlertType alertType, out byte encoded)
|
||||
{
|
||||
if (_typeToIndex.TryGetValue(alertType, out var idx))
|
||||
{
|
||||
encoded = idx;
|
||||
return true;
|
||||
}
|
||||
|
||||
encoded = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get the alert from the encoded representation
|
||||
/// </summary>
|
||||
/// <returns>true if successful</returns>
|
||||
public bool TryDecode(byte encodedAlert, out AlertPrototype alert)
|
||||
{
|
||||
if (encodedAlert >= _orderedAlerts.Length)
|
||||
{
|
||||
alert = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
alert = _orderedAlerts[encodedAlert];
|
||||
return true;
|
||||
return _typeToAlert.TryGetValue(alertType, out alert);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using Content.Shared.Interfaces;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
@@ -70,6 +72,11 @@ namespace Content.Shared.Alert
|
||||
/// </summary>
|
||||
public bool SupportsSeverity => MaxSeverity != -1;
|
||||
|
||||
/// <summary>
|
||||
/// Defines what to do when the alert is clicked.
|
||||
/// </summary>
|
||||
public IAlertClick OnClick { get; private set; }
|
||||
|
||||
public void LoadFrom(YamlMappingNode mapping)
|
||||
{
|
||||
var serializer = YamlObjectSerializer.NewReader(mapping);
|
||||
@@ -94,6 +101,9 @@ namespace Content.Shared.Alert
|
||||
Category = alertCategory;
|
||||
}
|
||||
AlertKey = new AlertKey(AlertType, Category);
|
||||
|
||||
if (IoCManager.Resolve<IModuleManager>().IsClientModule) return;
|
||||
serializer.DataField(this, x => x.OnClick, "onClick", null);
|
||||
}
|
||||
|
||||
/// <param name="severity">severity level, if supported by this alert</param>
|
||||
@@ -143,30 +153,26 @@ namespace Content.Shared.Alert
|
||||
[Serializable, NetSerializable]
|
||||
public struct AlertKey
|
||||
{
|
||||
private readonly AlertType? _alertType;
|
||||
private readonly AlertCategory? _alertCategory;
|
||||
public readonly AlertType? AlertType;
|
||||
public readonly AlertCategory? AlertCategory;
|
||||
|
||||
/// NOTE: if the alert has a category you must pass the category for this to work
|
||||
/// properly as a key. I.e. if the alert has a category and you pass only the ID, and you
|
||||
/// compare this to another AlertKey that has both the category and the same ID, it will not consider them equal.
|
||||
/// properly as a key. I.e. if the alert has a category and you pass only the alert type, and you
|
||||
/// compare this to another AlertKey that has both the category and the same alert type, it will not consider them equal.
|
||||
public AlertKey(AlertType? alertType, AlertCategory? alertCategory)
|
||||
{
|
||||
// if there is a category, ignore the alerttype.
|
||||
if (alertCategory != null)
|
||||
{
|
||||
_alertCategory = alertCategory;
|
||||
_alertType = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
_alertCategory = null;
|
||||
_alertType = alertType;
|
||||
}
|
||||
AlertCategory = alertCategory;
|
||||
AlertType = alertType;
|
||||
}
|
||||
|
||||
public bool Equals(AlertKey other)
|
||||
{
|
||||
return _alertType == other._alertType && _alertCategory == other._alertCategory;
|
||||
// compare only on alert category if we have one
|
||||
if (AlertCategory.HasValue)
|
||||
{
|
||||
return other.AlertCategory == AlertCategory;
|
||||
}
|
||||
return AlertType == other.AlertType && AlertCategory == other.AlertCategory;
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
@@ -176,11 +182,14 @@ namespace Content.Shared.Alert
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(_alertType, _alertCategory);
|
||||
// use only alert category if we have one
|
||||
if (AlertCategory.HasValue) return AlertCategory.GetHashCode();
|
||||
return AlertType.GetHashCode();
|
||||
}
|
||||
|
||||
/// <param name="category">alert category, must not be null</param>
|
||||
/// <returns>An alert key for the provided alert category</returns>
|
||||
/// <returns>An alert key for the provided alert category. This must only be used for
|
||||
/// queries and never storage, as it is lacking an alert type.</returns>
|
||||
public static AlertKey ForCategory(AlertCategory category)
|
||||
{
|
||||
return new(null, category);
|
||||
|
||||
@@ -16,8 +16,10 @@
|
||||
|
||||
/// <summary>
|
||||
/// Every kind of alert. Corresponds to alertType field in alert prototypes defined in YML
|
||||
/// NOTE: Using byte for a compact encoding when sending this in messages, can upgrade
|
||||
/// to ushort
|
||||
/// </summary>
|
||||
public enum AlertType
|
||||
public enum AlertType : byte
|
||||
{
|
||||
Error,
|
||||
LowPressure,
|
||||
|
||||
37
Content.Shared/Alert/IAlertClick.cs
Normal file
37
Content.Shared/Alert/IAlertClick.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Interfaces.Serialization;
|
||||
|
||||
namespace Content.Shared.Alert
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines what should happen when an alert is clicked.
|
||||
/// </summary>
|
||||
public interface IAlertClick : IExposeData
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Invoked on server side when user clicks an alert.
|
||||
/// </summary>
|
||||
/// <param name="args"></param>
|
||||
void AlertClicked(ClickAlertEventArgs args);
|
||||
}
|
||||
|
||||
public class ClickAlertEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Player clicking the alert
|
||||
/// </summary>
|
||||
public readonly IEntity Player;
|
||||
/// <summary>
|
||||
/// Alert that was clicked
|
||||
/// </summary>
|
||||
public readonly AlertPrototype Alert;
|
||||
|
||||
public ClickAlertEventArgs(IEntity player, AlertPrototype alert)
|
||||
{
|
||||
Player = player;
|
||||
Alert = alert;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Content.Shared.GameObjects.Components.Items;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
@@ -26,7 +27,7 @@ namespace Content.Shared.GameObjects.Components.Inventory
|
||||
/// Uniquely identifies a single slot in an inventory.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public enum Slots
|
||||
public enum Slots : byte
|
||||
{
|
||||
NONE = 0,
|
||||
HEAD,
|
||||
@@ -148,6 +149,5 @@ namespace Content.Shared.GameObjects.Components.Inventory
|
||||
"Hands_left",
|
||||
"Hands_right",
|
||||
};
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Interfaces.Reflection;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Serialization;
|
||||
@@ -47,6 +48,10 @@ namespace Content.Shared.GameObjects.Components.Inventory
|
||||
InventoryInstance = DynamicTypeFactory.CreateInstance<Inventory>(type);
|
||||
}
|
||||
|
||||
/// <returns>true if the item is equipped to an equip slot (NOT inside an equipped container
|
||||
/// like inside a backpack)</returns>
|
||||
public abstract bool IsEquipped(IEntity item);
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
protected class InventoryComponentState : ComponentState
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components.Items
|
||||
@@ -9,6 +10,9 @@ namespace Content.Shared.GameObjects.Components.Items
|
||||
{
|
||||
public sealed override string Name => "Hands";
|
||||
public sealed override uint? NetID => ContentNetIDs.HANDS;
|
||||
|
||||
/// <returns>true if the item is in one of the hands</returns>
|
||||
public abstract bool IsHolding(IEntity item);
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
|
||||
208
Content.Shared/GameObjects/Components/Mobs/IActionAttempt.cs
Normal file
208
Content.Shared/GameObjects/Components/Mobs/IActionAttempt.cs
Normal file
@@ -0,0 +1,208 @@
|
||||
#nullable enable
|
||||
|
||||
using Content.Shared.Actions;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Input.Binding;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components.Mobs
|
||||
{
|
||||
/// <summary>
|
||||
/// An attempt to perform a specific action. Main purpose of this interface is to
|
||||
/// reduce code duplication related to handling attempts to perform non-item vs item actions by
|
||||
/// providing a single interface for various functionality that needs to be performed on both.
|
||||
/// </summary>
|
||||
public interface IActionAttempt
|
||||
{
|
||||
/// <summary>
|
||||
/// Action Prototype attempting to be performed
|
||||
/// </summary>
|
||||
BaseActionPrototype Action { get; }
|
||||
ComponentMessage PerformInstantActionMessage();
|
||||
ComponentMessage PerformToggleActionMessage(bool on);
|
||||
ComponentMessage PerformTargetPointActionMessage(PointerInputCmdHandler.PointerInputCmdArgs args);
|
||||
ComponentMessage PerformTargetEntityActionMessage(PointerInputCmdHandler.PointerInputCmdArgs args);
|
||||
/// <summary>
|
||||
/// Tries to get the action state for this action from the actionsComponent, returning
|
||||
/// true if found.
|
||||
/// </summary>
|
||||
bool TryGetActionState(SharedActionsComponent actionsComponent, out ActionState actionState);
|
||||
|
||||
/// <summary>
|
||||
/// Toggles the action within the provided action component
|
||||
/// </summary>
|
||||
void ToggleAction(SharedActionsComponent actionsComponent, bool toggleOn);
|
||||
|
||||
/// <summary>
|
||||
/// Perform the server-side logic of the action
|
||||
/// </summary>
|
||||
void DoInstantAction(IEntity player);
|
||||
|
||||
/// <summary>
|
||||
/// Perform the server-side logic of the toggle action
|
||||
/// </summary>
|
||||
/// <returns>true if the attempt to toggle was successful, meaning the state should be toggled to the
|
||||
/// indicated value</returns>
|
||||
bool DoToggleAction(IEntity player, bool on);
|
||||
|
||||
/// <summary>
|
||||
/// Perform the server-side logic of the target point action
|
||||
/// </summary>
|
||||
void DoTargetPointAction(IEntity player, EntityCoordinates target);
|
||||
|
||||
/// <summary>
|
||||
/// Perform the server-side logic of the target entity action
|
||||
/// </summary>
|
||||
void DoTargetEntityAction(IEntity player, IEntity target);
|
||||
}
|
||||
|
||||
public class ActionAttempt : IActionAttempt
|
||||
{
|
||||
private readonly ActionPrototype _action;
|
||||
|
||||
public BaseActionPrototype Action => _action;
|
||||
|
||||
public ActionAttempt(ActionPrototype action)
|
||||
{
|
||||
_action = action;
|
||||
}
|
||||
|
||||
public bool TryGetActionState(SharedActionsComponent actionsComponent, out ActionState actionState)
|
||||
{
|
||||
return actionsComponent.TryGetActionState(_action.ActionType, out actionState);
|
||||
}
|
||||
|
||||
public void ToggleAction(SharedActionsComponent actionsComponent, bool toggleOn)
|
||||
{
|
||||
actionsComponent.ToggleAction(_action.ActionType, toggleOn);
|
||||
}
|
||||
|
||||
public void DoInstantAction(IEntity player)
|
||||
{
|
||||
_action.InstantAction.DoInstantAction(new InstantActionEventArgs(player, _action.ActionType));
|
||||
}
|
||||
|
||||
public bool DoToggleAction(IEntity player, bool on)
|
||||
{
|
||||
return _action.ToggleAction.DoToggleAction(new ToggleActionEventArgs(player, _action.ActionType, on));
|
||||
}
|
||||
|
||||
public void DoTargetPointAction(IEntity player, EntityCoordinates target)
|
||||
{
|
||||
_action.TargetPointAction.DoTargetPointAction(new TargetPointActionEventArgs(player, target, _action.ActionType));
|
||||
}
|
||||
|
||||
public void DoTargetEntityAction(IEntity player, IEntity target)
|
||||
{
|
||||
_action.TargetEntityAction.DoTargetEntityAction(new TargetEntityActionEventArgs(player, _action.ActionType,
|
||||
target));
|
||||
}
|
||||
|
||||
public ComponentMessage PerformInstantActionMessage()
|
||||
{
|
||||
return new PerformInstantActionMessage(_action.ActionType);
|
||||
}
|
||||
|
||||
public ComponentMessage PerformToggleActionMessage(bool toggleOn)
|
||||
{
|
||||
if (toggleOn)
|
||||
{
|
||||
return new PerformToggleOnActionMessage(_action.ActionType);
|
||||
}
|
||||
return new PerformToggleOffActionMessage(_action.ActionType);
|
||||
}
|
||||
|
||||
public ComponentMessage PerformTargetPointActionMessage(PointerInputCmdHandler.PointerInputCmdArgs args)
|
||||
{
|
||||
return new PerformTargetPointActionMessage(_action.ActionType, args.Coordinates);
|
||||
}
|
||||
|
||||
public ComponentMessage PerformTargetEntityActionMessage(PointerInputCmdHandler.PointerInputCmdArgs args)
|
||||
{
|
||||
return new PerformTargetEntityActionMessage(_action.ActionType, args.EntityUid);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{nameof(_action)}: {_action.ActionType}";
|
||||
}
|
||||
}
|
||||
|
||||
public class ItemActionAttempt : IActionAttempt
|
||||
{
|
||||
private readonly ItemActionPrototype _action;
|
||||
private readonly IEntity _item;
|
||||
private readonly ItemActionsComponent _itemActions;
|
||||
|
||||
public BaseActionPrototype Action => _action;
|
||||
|
||||
public ItemActionAttempt(ItemActionPrototype action, IEntity item, ItemActionsComponent itemActions)
|
||||
{
|
||||
_action = action;
|
||||
_item = item;
|
||||
_itemActions = itemActions;
|
||||
}
|
||||
|
||||
public void DoInstantAction(IEntity player)
|
||||
{
|
||||
_action.InstantAction.DoInstantAction(new InstantItemActionEventArgs(player, _item, _action.ActionType));
|
||||
}
|
||||
|
||||
public bool DoToggleAction(IEntity player, bool on)
|
||||
{
|
||||
return _action.ToggleAction.DoToggleAction(new ToggleItemActionEventArgs(player, on, _item, _action.ActionType));
|
||||
}
|
||||
|
||||
public void DoTargetPointAction(IEntity player, EntityCoordinates target)
|
||||
{
|
||||
_action.TargetPointAction.DoTargetPointAction(new TargetPointItemActionEventArgs(player, target, _item,
|
||||
_action.ActionType));
|
||||
}
|
||||
|
||||
public void DoTargetEntityAction(IEntity player, IEntity target)
|
||||
{
|
||||
_action.TargetEntityAction.DoTargetEntityAction(new TargetEntityItemActionEventArgs(player, target,
|
||||
_item, _action.ActionType));
|
||||
}
|
||||
|
||||
public bool TryGetActionState(SharedActionsComponent actionsComponent, out ActionState actionState)
|
||||
{
|
||||
return actionsComponent.TryGetItemActionState(_action.ActionType, _item, out actionState);
|
||||
}
|
||||
|
||||
public void ToggleAction(SharedActionsComponent actionsComponent, bool toggleOn)
|
||||
{
|
||||
_itemActions.Toggle(_action.ActionType, toggleOn);
|
||||
}
|
||||
|
||||
public ComponentMessage PerformInstantActionMessage()
|
||||
{
|
||||
return new PerformInstantItemActionMessage(_action.ActionType, _item.Uid);
|
||||
}
|
||||
|
||||
public ComponentMessage PerformToggleActionMessage(bool toggleOn)
|
||||
{
|
||||
if (toggleOn)
|
||||
{
|
||||
return new PerformToggleOnItemActionMessage(_action.ActionType, _item.Uid);
|
||||
}
|
||||
return new PerformToggleOffItemActionMessage(_action.ActionType, _item.Uid);
|
||||
}
|
||||
|
||||
public ComponentMessage PerformTargetPointActionMessage(PointerInputCmdHandler.PointerInputCmdArgs args)
|
||||
{
|
||||
return new PerformTargetPointItemActionMessage(_action.ActionType, _item.Uid, args.Coordinates);
|
||||
}
|
||||
|
||||
public ComponentMessage PerformTargetEntityActionMessage(PointerInputCmdHandler.PointerInputCmdArgs args)
|
||||
{
|
||||
return new PerformTargetEntityItemActionMessage(_action.ActionType, _item.Uid, args.EntityUid);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{nameof(_action)}: {_action.ActionType}, {nameof(_item)}: {_item}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.GameObjects.Components.Inventory;
|
||||
using Content.Shared.GameObjects.Components.Items;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Interfaces.Serialization;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components.Mobs
|
||||
{
|
||||
/// <summary>
|
||||
/// This should be used on items which provide actions. Defines which actions the item provides
|
||||
/// and allows modifying the states of those actions. Item components should use this rather than
|
||||
/// SharedActionsComponent on the player to handle granting / revoking / modifying the states of the
|
||||
/// actions provided by this item.
|
||||
///
|
||||
/// When a player equips this item, all the actions defined in this component will be granted to the
|
||||
/// player in their current states. This means the states will persist between players.
|
||||
///
|
||||
/// Currently only maintained server side and not synced to client, as are all the equip/unequip events.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public class ItemActionsComponent : Component, IEquippedHand, IEquipped, IUnequipped, IUnequippedHand
|
||||
{
|
||||
public override string Name => "ItemActions";
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for the item actions initially provided by this item. Actions defined here
|
||||
/// will be automatically granted unless their state is modified using the methods
|
||||
/// on this component. Additional actions can be granted by this item via GrantOrUpdate
|
||||
/// </summary>
|
||||
public IEnumerable<ItemActionConfig> ActionConfigs => _actionConfigs;
|
||||
|
||||
public bool IsEquipped => InSlot != EquipmentSlotDefines.Slots.NONE || InHand != null;
|
||||
/// <summary>
|
||||
/// Slot currently equipped to, NONE if not equipped to an equip slot.
|
||||
/// </summary>
|
||||
public EquipmentSlotDefines.Slots InSlot { get; private set; }
|
||||
/// <summary>
|
||||
/// hand it's currently in, null if not in a hand.
|
||||
/// </summary>
|
||||
public SharedHand? InHand { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Entity currently holding this in hand or equip slot. Null if not held.
|
||||
/// </summary>
|
||||
public IEntity? Holder { get; private set; }
|
||||
// cached actions component of the holder, since we'll need to access it frequently
|
||||
private SharedActionsComponent? _holderActionsComponent;
|
||||
private List<ItemActionConfig> _actionConfigs = new();
|
||||
// State of all actions provided by this item.
|
||||
private readonly Dictionary<ItemActionType, ActionState> _actions = new();
|
||||
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
|
||||
serializer.DataField(ref _actionConfigs,"actions", new List<ItemActionConfig>());
|
||||
|
||||
foreach (var actionConfig in _actionConfigs)
|
||||
{
|
||||
GrantOrUpdate(actionConfig.ActionType, actionConfig.Enabled, false, null);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Startup()
|
||||
{
|
||||
base.Startup();
|
||||
GrantOrUpdateAllToHolder();
|
||||
}
|
||||
|
||||
protected override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
RevokeAllFromHolder();
|
||||
}
|
||||
|
||||
private void GrantOrUpdateAllToHolder()
|
||||
{
|
||||
if (_holderActionsComponent == null) return;
|
||||
foreach (var (actionType, state) in _actions)
|
||||
{
|
||||
_holderActionsComponent.GrantOrUpdateItemAction(actionType, Owner.Uid, state);
|
||||
}
|
||||
}
|
||||
|
||||
private void RevokeAllFromHolder()
|
||||
{
|
||||
if (_holderActionsComponent == null) return;
|
||||
foreach (var (actionType, state) in _actions)
|
||||
{
|
||||
_holderActionsComponent.RevokeItemAction(actionType, Owner.Uid);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the state of the action, granting it if it isn't already granted.
|
||||
/// If the action had any existing state, those specific fields will be overwritten by any
|
||||
/// corresponding non-null arguments.
|
||||
/// </summary>
|
||||
/// <param name="actionType">action being granted / updated</param>
|
||||
/// <param name="enabled">When null, preserves the current enable status of the action, defaulting
|
||||
/// to true if action has no current state.
|
||||
/// When non-null, indicates whether the entity is able to perform the action (if disabled,
|
||||
/// the player will see they have the action but it will appear greyed out)</param>
|
||||
/// <param name="toggleOn">When null, preserves the current toggle status of the action, defaulting
|
||||
/// to false if action has no current state.
|
||||
/// When non-null, action will be shown toggled to this value</param>
|
||||
/// <param name="cooldown"> When null (unless clearCooldown is true), preserves the current cooldown status of the action, defaulting
|
||||
/// to no cooldown if action has no current state.
|
||||
/// When non-null or clearCooldown is true, action cooldown will be set to this value. Note that this cooldown
|
||||
/// is tied to this item.</param>
|
||||
/// <param name="clearCooldown"> If true, setting cooldown to null will clear the current cooldown
|
||||
/// of this action rather than preserving it.</param>
|
||||
public void GrantOrUpdate(ItemActionType actionType, bool? enabled = null,
|
||||
bool? toggleOn = null,
|
||||
(TimeSpan start, TimeSpan end)? cooldown = null, bool clearCooldown = false)
|
||||
{
|
||||
var dirty = false;
|
||||
// this will be overwritten if we find the value in our dict, otherwise
|
||||
// we will use this as our new action state.
|
||||
if (!_actions.TryGetValue(actionType, out var actionState))
|
||||
{
|
||||
dirty = true;
|
||||
actionState = new ActionState(enabled ?? true, toggleOn ?? false);
|
||||
}
|
||||
|
||||
if (enabled.HasValue && enabled != actionState.Enabled)
|
||||
{
|
||||
dirty = true;
|
||||
actionState.Enabled = true;
|
||||
}
|
||||
|
||||
if ((cooldown.HasValue || clearCooldown) && actionState.Cooldown != cooldown)
|
||||
{
|
||||
dirty = true;
|
||||
actionState.Cooldown = cooldown;
|
||||
}
|
||||
|
||||
if (toggleOn.HasValue && actionState.ToggledOn != toggleOn.Value)
|
||||
{
|
||||
dirty = true;
|
||||
actionState.ToggledOn = toggleOn.Value;
|
||||
}
|
||||
|
||||
if (!dirty) return;
|
||||
|
||||
_actions[actionType] = actionState;
|
||||
_holderActionsComponent?.GrantOrUpdateItemAction(actionType, Owner.Uid, actionState);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the cooldown of a particular action. Actions on cooldown cannot be used.
|
||||
/// Setting the cooldown to null clears it.
|
||||
/// </summary>
|
||||
public void Cooldown(ItemActionType actionType, (TimeSpan start, TimeSpan end)? cooldown = null)
|
||||
{
|
||||
GrantOrUpdate(actionType, cooldown: cooldown, clearCooldown: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enable / disable this action. Disabled actions are still shown to the player, but
|
||||
/// shown as not usable.
|
||||
/// </summary>
|
||||
public void SetEnabled(ItemActionType actionType, bool enabled)
|
||||
{
|
||||
GrantOrUpdate(actionType, enabled);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Toggle the action on / off
|
||||
/// </summary>
|
||||
public void Toggle(ItemActionType actionType, bool toggleOn)
|
||||
{
|
||||
GrantOrUpdate(actionType, toggleOn: toggleOn);
|
||||
}
|
||||
|
||||
public void EquippedHand(EquippedHandEventArgs eventArgs)
|
||||
{
|
||||
// this entity cannot be granted actions if no actions component
|
||||
if (!eventArgs.User.TryGetComponent<SharedActionsComponent>(out var actionsComponent))
|
||||
return;
|
||||
Holder = eventArgs.User;
|
||||
_holderActionsComponent = actionsComponent;
|
||||
InSlot = EquipmentSlotDefines.Slots.NONE;
|
||||
InHand = eventArgs.Hand;
|
||||
GrantOrUpdateAllToHolder();
|
||||
}
|
||||
|
||||
public void Equipped(EquippedEventArgs eventArgs)
|
||||
{
|
||||
// this entity cannot be granted actions if no actions component
|
||||
if (!eventArgs.User.TryGetComponent<SharedActionsComponent>(out var actionsComponent))
|
||||
return;
|
||||
Holder = eventArgs.User;
|
||||
_holderActionsComponent = actionsComponent;
|
||||
InSlot = eventArgs.Slot;
|
||||
InHand = null;
|
||||
GrantOrUpdateAllToHolder();
|
||||
}
|
||||
|
||||
public void Unequipped(UnequippedEventArgs eventArgs)
|
||||
{
|
||||
RevokeAllFromHolder();
|
||||
Holder = null;
|
||||
_holderActionsComponent = null;
|
||||
InSlot = EquipmentSlotDefines.Slots.NONE;
|
||||
InHand = null;
|
||||
|
||||
}
|
||||
|
||||
public void UnequippedHand(UnequippedHandEventArgs eventArgs)
|
||||
{
|
||||
RevokeAllFromHolder();
|
||||
Holder = null;
|
||||
_holderActionsComponent = null;
|
||||
InSlot = EquipmentSlotDefines.Slots.NONE;
|
||||
InHand = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for an item action provided by an item.
|
||||
/// </summary>
|
||||
public class ItemActionConfig : IExposeData
|
||||
{
|
||||
public ItemActionType ActionType { get; private set; }
|
||||
/// <summary>
|
||||
/// Whether action is initially enabled on this item. Defaults to true.
|
||||
/// </summary>
|
||||
public bool Enabled { get; private set; }
|
||||
|
||||
public void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
serializer.DataField(this, x => x.ActionType, "actionType", ItemActionType.Error);
|
||||
if (ActionType == ItemActionType.Error)
|
||||
{
|
||||
Logger.ErrorS("action", "invalid or missing actionType");
|
||||
}
|
||||
serializer.DataField(this, x => x.Enabled, "enabled", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,652 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Content.Shared.Actions;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.Interfaces.Timing;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components.Mobs
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages the actions available to an entity.
|
||||
/// Should only be used for player-controlled entities.
|
||||
///
|
||||
/// Actions are granted directly to the owner entity. Item actions are granted via a particular item which
|
||||
/// must be in the owner's inventory (the action is revoked when item leaves the owner's inventory). This
|
||||
/// should almost always be done via ItemActionsComponent on the item entity (which also tracks the
|
||||
/// cooldowns associated with the actions on that item).
|
||||
///
|
||||
/// Actions can still have an associated state even when revoked. For example, a flashlight toggle action
|
||||
/// may be unusable while the player is stunned, but this component will still have an entry for the action
|
||||
/// so the user can see whether it's currently toggled on or off.
|
||||
/// </summary>
|
||||
public abstract class SharedActionsComponent : Component
|
||||
{
|
||||
private static readonly TimeSpan CooldownExpiryThreshold = TimeSpan.FromSeconds(10);
|
||||
|
||||
[Dependency]
|
||||
protected readonly ActionManager ActionManager = default!;
|
||||
[Dependency]
|
||||
protected readonly IGameTiming GameTiming = default!;
|
||||
[Dependency]
|
||||
protected readonly IEntityManager EntityManager = default!;
|
||||
|
||||
public override string Name => "Actions";
|
||||
public override uint? NetID => ContentNetIDs.ACTIONS;
|
||||
|
||||
/// <summary>
|
||||
/// Actions granted to this entity as soon as they spawn, regardless
|
||||
/// of the status of the entity.
|
||||
/// </summary>
|
||||
public IEnumerable<ActionType> InnateActions => _innateActions ?? Enumerable.Empty<ActionType>();
|
||||
private List<ActionType>? _innateActions;
|
||||
|
||||
|
||||
// entries are removed from this if they are at the initial state (not enabled, no cooldown, toggled off).
|
||||
// a system runs which periodically removes cooldowns from entries when they are revoked and their
|
||||
// cooldowns have expired for a long enough time, also removing the entry if it is then at initial state.
|
||||
// This helps to keep our component state smaller.
|
||||
[ViewVariables]
|
||||
private Dictionary<ActionType, ActionState> _actions = new();
|
||||
|
||||
// Holds item action states. Item actions are only added to this when granted, and are removed
|
||||
// when revoked or when they leave inventory. This is almost entirely handled by ItemActionsComponent on
|
||||
// item entities.
|
||||
[ViewVariables]
|
||||
private Dictionary<EntityUid, Dictionary<ItemActionType, ActionState>> _itemActions =
|
||||
new();
|
||||
|
||||
public override void ExposeData(ObjectSerializer serializer)
|
||||
{
|
||||
base.ExposeData(serializer);
|
||||
serializer.DataField(ref _innateActions,"innateActions", null);
|
||||
}
|
||||
protected override void Startup()
|
||||
{
|
||||
foreach (var actionType in InnateActions)
|
||||
{
|
||||
Grant(actionType);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public override ComponentState GetComponentState()
|
||||
{
|
||||
return new ActionComponentState(_actions, _itemActions);
|
||||
}
|
||||
|
||||
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
|
||||
{
|
||||
base.HandleComponentState(curState, nextState);
|
||||
|
||||
if (curState is not ActionComponentState state)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_actions = state.Actions;
|
||||
_itemActions = state.ItemActions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the action state associated with the specified action type, if it has been
|
||||
/// granted, has a cooldown, or has been toggled on
|
||||
/// </summary>
|
||||
/// <returns>false if not found for this action type</returns>
|
||||
public bool TryGetActionState(ActionType actionType, out ActionState actionState)
|
||||
{
|
||||
return _actions.TryGetValue(actionType, out actionState);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the item action states associated with the specified item if any have been granted
|
||||
/// and not yet revoked.
|
||||
/// </summary>
|
||||
/// <returns>false if no states found for this item action type.</returns>
|
||||
public bool TryGetItemActionStates(EntityUid item, [NotNullWhen((true))] out IReadOnlyDictionary<ItemActionType, ActionState>? itemActionStates)
|
||||
{
|
||||
if (_itemActions.TryGetValue(item, out var actualItemActionStates))
|
||||
{
|
||||
itemActionStates = actualItemActionStates;
|
||||
return true;
|
||||
}
|
||||
|
||||
itemActionStates = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <seealso cref="TryGetItemActionStates(Robust.Shared.GameObjects.EntityUid,out System.Collections.Generic.IReadOnlyDictionary{Content.Shared.Actions.ItemActionType,Content.Shared.GameObjects.Components.Mobs.ActionState}?)"/>
|
||||
public bool TryGetItemActionStates(IEntity item,
|
||||
[NotNullWhen((true))] out IReadOnlyDictionary<ItemActionType, ActionState>? itemActionStates)
|
||||
{
|
||||
return TryGetItemActionStates(item.Uid, out itemActionStates);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the item action state associated with the specified item action type for the specified item, if it has any.
|
||||
/// </summary>
|
||||
/// <returns>false if no state found for this item action type for this item</returns>
|
||||
public bool TryGetItemActionState(ItemActionType actionType, EntityUid item, out ActionState actionState)
|
||||
{
|
||||
if (_itemActions.TryGetValue(item, out var actualItemActionStates))
|
||||
{
|
||||
return actualItemActionStates.TryGetValue(actionType, out actionState);
|
||||
}
|
||||
|
||||
actionState = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <returns>true if the action is granted and enabled (if item action, if granted and enabled for any item)</returns>
|
||||
public bool IsGranted(BaseActionPrototype actionType)
|
||||
{
|
||||
return actionType switch
|
||||
{
|
||||
ActionPrototype actionPrototype => IsGranted(actionPrototype.ActionType),
|
||||
ItemActionPrototype itemActionPrototype => IsGranted(itemActionPrototype.ActionType),
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
public bool IsGranted(ActionType actionType)
|
||||
{
|
||||
if (TryGetActionState(actionType, out var actionState))
|
||||
{
|
||||
return actionState.Enabled;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <returns>true if the action is granted and enabled for any item. This
|
||||
/// has to traverse the entire item state dictionary so please avoid frequent calls.</returns>
|
||||
public bool IsGranted(ItemActionType actionType)
|
||||
{
|
||||
return _itemActions.Values.SelectMany(vals => vals)
|
||||
.Any(state => state.Key == actionType && state.Value.Enabled);
|
||||
}
|
||||
|
||||
/// <seealso cref="TryGetItemActionState(Content.Shared.Actions.ItemActionType,Robust.Shared.GameObjects.EntityUid,out Content.Shared.GameObjects.Components.Mobs.ActionState)"/>
|
||||
public bool TryGetItemActionState(ItemActionType actionType, IEntity item, out ActionState actionState)
|
||||
{
|
||||
return TryGetItemActionState(actionType, item.Uid, out actionState);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all action types that have non-initial state (granted, have a cooldown, or toggled on).
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<ActionType, ActionState> ActionStates()
|
||||
{
|
||||
return _actions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all items that have actions currently granted (that are not revoked
|
||||
/// and still in inventory).
|
||||
/// Map from item uid -> (action type -> associated action state)
|
||||
/// PLEASE DO NOT MODIFY THE INNER DICTIONARY! I CANNOT CAST IT TO IReadOnlyDictionary!
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<EntityUid,Dictionary<ItemActionType, ActionState>> ItemActionStates()
|
||||
{
|
||||
return _itemActions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates or updates the action state with the supplied non-null values
|
||||
/// </summary>
|
||||
private void GrantOrUpdate(ActionType actionType, bool? enabled = null, bool? toggleOn = null,
|
||||
(TimeSpan start, TimeSpan end)? cooldown = null, bool clearCooldown = false)
|
||||
{
|
||||
var dirty = false;
|
||||
if (!_actions.TryGetValue(actionType, out var actionState))
|
||||
{
|
||||
// no state at all for this action, create it anew
|
||||
dirty = true;
|
||||
actionState = new ActionState(enabled ?? false, toggleOn ?? false);
|
||||
}
|
||||
|
||||
if (enabled.HasValue && actionState.Enabled != enabled.Value)
|
||||
{
|
||||
dirty = true;
|
||||
actionState.Enabled = enabled.Value;
|
||||
}
|
||||
|
||||
if ((cooldown.HasValue || clearCooldown) && actionState.Cooldown != cooldown)
|
||||
{
|
||||
dirty = true;
|
||||
actionState.Cooldown = cooldown;
|
||||
}
|
||||
|
||||
if (toggleOn.HasValue && actionState.ToggledOn != toggleOn.Value)
|
||||
{
|
||||
dirty = true;
|
||||
actionState.ToggledOn = toggleOn.Value;
|
||||
}
|
||||
|
||||
if (!dirty) return;
|
||||
|
||||
_actions[actionType] = actionState;
|
||||
AfterActionChanged();
|
||||
Dirty();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Intended to only be used by ItemActionsComponent.
|
||||
/// Updates the state of the item action provided by the item, granting the action
|
||||
/// if it is not yet granted to the player. Should be called whenever the
|
||||
/// status changes. The existing state will be completely overwritten by the new state.
|
||||
/// </summary>
|
||||
public void GrantOrUpdateItemAction(ItemActionType actionType, EntityUid item, ActionState state)
|
||||
{
|
||||
if (!_itemActions.TryGetValue(item, out var itemStates))
|
||||
{
|
||||
itemStates = new Dictionary<ItemActionType, ActionState>();
|
||||
_itemActions[item] = itemStates;
|
||||
}
|
||||
|
||||
itemStates[actionType] = state;
|
||||
AfterActionChanged();
|
||||
Dirty();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Intended to only be used by ItemActionsComponent. Revokes the item action so the player no longer
|
||||
/// sees it and can no longer use it.
|
||||
/// </summary>
|
||||
public void RevokeItemAction(ItemActionType actionType, EntityUid item)
|
||||
{
|
||||
if (!_itemActions.TryGetValue(item, out var itemStates))
|
||||
return;
|
||||
|
||||
itemStates.Remove(actionType);
|
||||
AfterActionChanged();
|
||||
Dirty();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Grants the entity the ability to perform the action, optionally overriding its
|
||||
/// current state with specified values.
|
||||
///
|
||||
/// Even if the action was already granted, if the action had any state (cooldown, toggle) prior to this method
|
||||
/// being called, it will be preserved, with specific fields optionally overridden by any of the provided
|
||||
/// non-null arguments.
|
||||
/// </summary>
|
||||
/// <param name="toggleOn">When null, preserves the current toggle status of the action, defaulting
|
||||
/// to false if action has no current state.
|
||||
/// When non-null, action will be shown toggled to this value</param>
|
||||
/// <param name="cooldown"> When null, preserves the current cooldown status of the action, defaulting
|
||||
/// to no cooldown if action has no current state.
|
||||
/// When non-null, action cooldown will be set to this value.</param>
|
||||
public void Grant(ActionType actionType, bool? toggleOn = null,
|
||||
(TimeSpan start, TimeSpan end)? cooldown = null)
|
||||
{
|
||||
GrantOrUpdate(actionType, true, toggleOn, cooldown);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Grants the entity the ability to perform the action, resetting its state
|
||||
/// to its initial state and settings its state based on supplied parameters.
|
||||
///
|
||||
/// Even if the action was already granted, if the action had any state (cooldown, toggle) prior to this method
|
||||
/// being called, it will be reset to initial (no cooldown, toggled off).
|
||||
/// </summary>
|
||||
/// <param name="toggleOn">action will be shown toggled to this value</param>
|
||||
/// <param name="cooldown">action cooldown will be set to this value (by default the cooldown is cleared).</param>
|
||||
public void GrantFromInitialState(ActionType actionType, bool toggleOn = false,
|
||||
(TimeSpan start, TimeSpan end)? cooldown = null)
|
||||
{
|
||||
_actions.Remove(actionType);
|
||||
Grant(actionType, toggleOn, cooldown);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the cooldown for the action. Actions on cooldown cannot be used.
|
||||
///
|
||||
/// This will work even if the action is revoked -
|
||||
/// for example if there's an ability with a cooldown which is temporarily unusable due
|
||||
/// to the player being stunned, the cooldown will still tick down even while the player
|
||||
/// is stunned.
|
||||
///
|
||||
/// Setting cooldown to null clears it.
|
||||
/// </summary>
|
||||
public void Cooldown(ActionType actionType, (TimeSpan start, TimeSpan end)? cooldown)
|
||||
{
|
||||
GrantOrUpdate(actionType, cooldown: cooldown, clearCooldown: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Revokes the ability to perform the action for this entity. Current state
|
||||
/// of the action (toggle / cooldown) is preserved.
|
||||
/// </summary>
|
||||
public void Revoke(ActionType actionType)
|
||||
{
|
||||
if (!_actions.TryGetValue(actionType, out var actionState)) return;
|
||||
|
||||
if (!actionState.Enabled) return;
|
||||
|
||||
actionState.Enabled = false;
|
||||
|
||||
// don't store it anymore if its at its initial state.
|
||||
if (actionState.IsAtInitialState)
|
||||
{
|
||||
_actions.Remove(actionType);
|
||||
}
|
||||
else
|
||||
{
|
||||
_actions[actionType] = actionState;
|
||||
}
|
||||
|
||||
AfterActionChanged();
|
||||
Dirty();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Toggles the action to the specified value. Works even if the action is on cooldown
|
||||
/// or revoked.
|
||||
/// </summary>
|
||||
public void ToggleAction(ActionType actionType, bool toggleOn)
|
||||
{
|
||||
Grant(actionType, toggleOn);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears any cooldowns which have expired beyond the predefined threshold.
|
||||
/// this should be run periodically to ensure we don't have unbounded growth of
|
||||
/// our saved action data, and keep our component state sent to the client as minimal as possible.
|
||||
/// </summary>
|
||||
public void ExpireCooldowns()
|
||||
{
|
||||
|
||||
// actions - only clear cooldowns and remove associated action state
|
||||
// if the action is at initial state
|
||||
var actionTypesToRemove = new List<ActionType>();
|
||||
foreach (var (actionType, actionState) in _actions)
|
||||
{
|
||||
// ignore it unless we may be able to delete it due to
|
||||
// clearing the cooldown
|
||||
if (!actionState.IsAtInitialStateExceptCooldown) continue;
|
||||
if (!actionState.Cooldown.HasValue)
|
||||
{
|
||||
actionTypesToRemove.Add(actionType);
|
||||
continue;
|
||||
}
|
||||
var expiryTime = GameTiming.CurTime - actionState.Cooldown.Value.Item2;
|
||||
if (expiryTime > CooldownExpiryThreshold)
|
||||
{
|
||||
actionTypesToRemove.Add(actionType);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var remove in actionTypesToRemove)
|
||||
{
|
||||
_actions.Remove(remove);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked after a change has been made to an action state in this component.
|
||||
/// </summary>
|
||||
protected virtual void AfterActionChanged() { }
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public class ActionComponentState : ComponentState
|
||||
{
|
||||
public Dictionary<ActionType, ActionState> Actions;
|
||||
public Dictionary<EntityUid, Dictionary<ItemActionType, ActionState>> ItemActions;
|
||||
|
||||
public ActionComponentState(Dictionary<ActionType, ActionState> actions,
|
||||
Dictionary<EntityUid, Dictionary<ItemActionType, ActionState>> itemActions) : base(ContentNetIDs.ACTIONS)
|
||||
{
|
||||
Actions = actions;
|
||||
ItemActions = itemActions;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public struct ActionState
|
||||
{
|
||||
/// <summary>
|
||||
/// False if this action is not currently allowed to be performed.
|
||||
/// </summary>
|
||||
public bool Enabled;
|
||||
/// <summary>
|
||||
/// Only used for toggle actions, indicates whether it's currently toggled on or off
|
||||
/// TODO: Eventually this should probably be a byte so we it can toggle through multiple states.
|
||||
/// </summary>
|
||||
public bool ToggledOn;
|
||||
public (TimeSpan start, TimeSpan end)? Cooldown;
|
||||
public bool IsAtInitialState => IsAtInitialStateExceptCooldown && !Cooldown.HasValue;
|
||||
public bool IsAtInitialStateExceptCooldown => !Enabled && !ToggledOn;
|
||||
|
||||
/// <summary>
|
||||
/// Creates an action state for the indicated type, defaulting to the
|
||||
/// initial state.
|
||||
/// </summary>
|
||||
public ActionState(bool enabled = false, bool toggledOn = false, (TimeSpan start, TimeSpan end)? cooldown = null)
|
||||
{
|
||||
Enabled = enabled;
|
||||
ToggledOn = toggledOn;
|
||||
Cooldown = cooldown;
|
||||
}
|
||||
|
||||
public bool IsOnCooldown(TimeSpan curTime)
|
||||
{
|
||||
if (Cooldown == null) return false;
|
||||
return curTime < Cooldown.Value.Item2;
|
||||
}
|
||||
public bool IsOnCooldown(IGameTiming gameTiming)
|
||||
{
|
||||
return IsOnCooldown(gameTiming.CurTime);
|
||||
}
|
||||
|
||||
public bool Equals(ActionState other)
|
||||
{
|
||||
return Enabled == other.Enabled && ToggledOn == other.ToggledOn && Nullable.Equals(Cooldown, other.Cooldown);
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is ActionState other && Equals(other);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Enabled, ToggledOn, Cooldown);
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public abstract class BasePerformActionMessage : ComponentMessage
|
||||
{
|
||||
public abstract BehaviorType BehaviorType { get; }
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public abstract class PerformActionMessage : BasePerformActionMessage
|
||||
{
|
||||
public readonly ActionType ActionType;
|
||||
|
||||
protected PerformActionMessage(ActionType actionType)
|
||||
{
|
||||
Directed = true;
|
||||
ActionType = actionType;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public abstract class PerformItemActionMessage : BasePerformActionMessage
|
||||
{
|
||||
public readonly ItemActionType ActionType;
|
||||
public readonly EntityUid Item;
|
||||
|
||||
protected PerformItemActionMessage(ItemActionType actionType, EntityUid item)
|
||||
{
|
||||
Directed = true;
|
||||
ActionType = actionType;
|
||||
Item = item;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A message that tells server we want to run the instant action logic.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public class PerformInstantActionMessage : PerformActionMessage
|
||||
{
|
||||
public override BehaviorType BehaviorType => BehaviorType.Instant;
|
||||
|
||||
public PerformInstantActionMessage(ActionType actionType) : base(actionType)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A message that tells server we want to run the instant action logic.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public class PerformInstantItemActionMessage : PerformItemActionMessage
|
||||
{
|
||||
public override BehaviorType BehaviorType => BehaviorType.Instant;
|
||||
|
||||
public PerformInstantItemActionMessage(ItemActionType actionType, EntityUid item) : base(actionType, item)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public interface IToggleActionMessage
|
||||
{
|
||||
bool ToggleOn { get; }
|
||||
}
|
||||
|
||||
public interface ITargetPointActionMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Targeted local coordinates
|
||||
/// </summary>
|
||||
EntityCoordinates Target { get; }
|
||||
}
|
||||
|
||||
public interface ITargetEntityActionMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Targeted entity
|
||||
/// </summary>
|
||||
EntityUid Target { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A message that tells server we want to toggle on the indicated action.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public class PerformToggleOnActionMessage : PerformActionMessage, IToggleActionMessage
|
||||
{
|
||||
public override BehaviorType BehaviorType => BehaviorType.Toggle;
|
||||
public bool ToggleOn => true;
|
||||
public PerformToggleOnActionMessage(ActionType actionType) : base(actionType) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A message that tells server we want to toggle off the indicated action.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public class PerformToggleOffActionMessage : PerformActionMessage, IToggleActionMessage
|
||||
{
|
||||
public override BehaviorType BehaviorType => BehaviorType.Toggle;
|
||||
public bool ToggleOn => false;
|
||||
public PerformToggleOffActionMessage(ActionType actionType) : base(actionType) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A message that tells server we want to toggle on the indicated action.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public class PerformToggleOnItemActionMessage : PerformItemActionMessage, IToggleActionMessage
|
||||
{
|
||||
public override BehaviorType BehaviorType => BehaviorType.Toggle;
|
||||
public bool ToggleOn => true;
|
||||
public PerformToggleOnItemActionMessage(ItemActionType actionType, EntityUid item) : base(actionType, item) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A message that tells server we want to toggle off the indicated action.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public class PerformToggleOffItemActionMessage : PerformItemActionMessage, IToggleActionMessage
|
||||
{
|
||||
public override BehaviorType BehaviorType => BehaviorType.Toggle;
|
||||
public bool ToggleOn => false;
|
||||
public PerformToggleOffItemActionMessage(ItemActionType actionType, EntityUid item) : base(actionType, item) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A message that tells server we want to target the provided point with a particular action.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public class PerformTargetPointActionMessage : PerformActionMessage, ITargetPointActionMessage
|
||||
{
|
||||
public override BehaviorType BehaviorType => BehaviorType.TargetPoint;
|
||||
private readonly EntityCoordinates _target;
|
||||
public EntityCoordinates Target => _target;
|
||||
|
||||
public PerformTargetPointActionMessage(ActionType actionType, EntityCoordinates target) : base(actionType)
|
||||
{
|
||||
_target = target;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A message that tells server we want to target the provided point with a particular action.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public class PerformTargetPointItemActionMessage : PerformItemActionMessage, ITargetPointActionMessage
|
||||
{
|
||||
private readonly EntityCoordinates _target;
|
||||
public EntityCoordinates Target => _target;
|
||||
public override BehaviorType BehaviorType => BehaviorType.TargetPoint;
|
||||
|
||||
public PerformTargetPointItemActionMessage(ItemActionType actionType, EntityUid item, EntityCoordinates target) : base(actionType, item)
|
||||
{
|
||||
_target = target;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A message that tells server we want to target the provided entity with a particular action.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public class PerformTargetEntityActionMessage : PerformActionMessage, ITargetEntityActionMessage
|
||||
{
|
||||
public override BehaviorType BehaviorType => BehaviorType.TargetEntity;
|
||||
private readonly EntityUid _target;
|
||||
public EntityUid Target => _target;
|
||||
|
||||
public PerformTargetEntityActionMessage(ActionType actionType, EntityUid target) : base(actionType)
|
||||
{
|
||||
_target = target;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A message that tells server we want to target the provided entity with a particular action.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public class PerformTargetEntityItemActionMessage : PerformItemActionMessage, ITargetEntityActionMessage
|
||||
{
|
||||
public override BehaviorType BehaviorType => BehaviorType.TargetEntity;
|
||||
private readonly EntityUid _target;
|
||||
public EntityUid Target => _target;
|
||||
|
||||
public PerformTargetEntityItemActionMessage(ItemActionType actionType, EntityUid item, EntityUid target) : base(actionType, item)
|
||||
{
|
||||
_target = target;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Shared.Alert;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Shared.GameObjects.Components.Mobs
|
||||
@@ -18,16 +15,30 @@ namespace Content.Shared.GameObjects.Components.Mobs
|
||||
/// </summary>
|
||||
public abstract class SharedAlertsComponent : Component
|
||||
{
|
||||
private static readonly AlertState[] NO_ALERTS = new AlertState[0];
|
||||
|
||||
[Dependency]
|
||||
protected readonly AlertManager AlertManager = default!;
|
||||
|
||||
public override string Name => "AlertsUI";
|
||||
public override string Name => "Alerts";
|
||||
public override uint? NetID => ContentNetIDs.ALERTS;
|
||||
|
||||
[ViewVariables]
|
||||
private Dictionary<AlertKey, ClickableAlertState> _alerts = new();
|
||||
[ViewVariables] private Dictionary<AlertKey, AlertState> _alerts = new();
|
||||
|
||||
public override void HandleComponentState(ComponentState curState, ComponentState nextState)
|
||||
{
|
||||
base.HandleComponentState(curState, nextState);
|
||||
|
||||
if (curState is not AlertsComponentState state)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_alerts = state.Alerts;
|
||||
}
|
||||
|
||||
public override ComponentState GetComponentState()
|
||||
{
|
||||
return new AlertsComponentState(_alerts);
|
||||
}
|
||||
|
||||
/// <returns>true iff an alert of the indicated alert category is currently showing</returns>
|
||||
public bool IsShowingAlertCategory(AlertCategory alertCategory)
|
||||
@@ -53,82 +64,14 @@ namespace Content.Shared.GameObjects.Components.Mobs
|
||||
return _alerts.ContainsKey(alertKey);
|
||||
}
|
||||
|
||||
protected IEnumerable<AlertState> EnumerateAlertStates()
|
||||
protected IEnumerable<KeyValuePair<AlertKey, AlertState>> EnumerateAlertStates()
|
||||
{
|
||||
return _alerts.Values.Select(alertData => alertData.AlertState);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes the alert's specified callback if there is one.
|
||||
/// Not intended to be used on clientside.
|
||||
/// </summary>
|
||||
protected void PerformAlertClickCallback(AlertPrototype alert, IEntity owner)
|
||||
{
|
||||
if (_alerts.TryGetValue(alert.AlertKey, out var alertStateCallback))
|
||||
{
|
||||
alertStateCallback.OnClickAlert?.Invoke(new ClickAlertEventArgs(owner, alert));
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.DebugS("alert", "player {0} attempted to invoke" +
|
||||
" alert click for {1} but that alert is not currently" +
|
||||
" showing", owner.Name, alert.AlertType);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new array containing all of the current alert states.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected AlertState[] CreateAlertStatesArray()
|
||||
{
|
||||
if (_alerts.Count == 0) return NO_ALERTS;
|
||||
var states = new AlertState[_alerts.Count];
|
||||
// because I don't trust LINQ
|
||||
var idx = 0;
|
||||
foreach (var alertData in _alerts.Values)
|
||||
{
|
||||
states[idx++] = alertData.AlertState;
|
||||
}
|
||||
|
||||
return states;
|
||||
return _alerts;
|
||||
}
|
||||
|
||||
protected bool TryGetAlertState(AlertKey key, out AlertState alertState)
|
||||
{
|
||||
if (_alerts.TryGetValue(key, out var alertData))
|
||||
{
|
||||
alertState = alertData.AlertState;
|
||||
return true;
|
||||
}
|
||||
|
||||
alertState = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replace the current active alerts with the specified alerts. Any
|
||||
/// OnClickAlert callbacks on the active alerts will be erased.
|
||||
/// </summary>
|
||||
protected void SetAlerts(AlertState[] alerts)
|
||||
{
|
||||
var newAlerts = new Dictionary<AlertKey, ClickableAlertState>();
|
||||
foreach (var alertState in alerts)
|
||||
{
|
||||
if (AlertManager.TryDecode(alertState.AlertEncoded, out var alert))
|
||||
{
|
||||
newAlerts[alert.AlertKey] = new ClickableAlertState
|
||||
{
|
||||
AlertState = alertState
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.ErrorS("alert", "unrecognized encoded alert {0}", alertState.AlertEncoded);
|
||||
}
|
||||
}
|
||||
|
||||
_alerts = newAlerts;
|
||||
return _alerts.TryGetValue(key, out alertState);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -136,30 +79,24 @@ namespace Content.Shared.GameObjects.Components.Mobs
|
||||
/// it will be updated / replaced with the specified values.
|
||||
/// </summary>
|
||||
/// <param name="alertType">type of the alert to set</param>
|
||||
/// <param name="onClickAlert">callback to invoke when ClickAlertMessage is received by the server
|
||||
/// after being clicked by client. Has no effect when specified on the clientside.</param>
|
||||
/// <param name="severity">severity, if supported by the alert</param>
|
||||
/// <param name="cooldown">cooldown start and end, if null there will be no cooldown (and it will
|
||||
/// be erased if there is currently a cooldown for the alert)</param>
|
||||
public void ShowAlert(AlertType alertType, short? severity = null, OnClickAlert onClickAlert = null,
|
||||
ValueTuple<TimeSpan, TimeSpan>? cooldown = null)
|
||||
public void ShowAlert(AlertType alertType, short? severity = null, ValueTuple<TimeSpan, TimeSpan>? cooldown = null)
|
||||
{
|
||||
if (AlertManager.TryGetWithEncoded(alertType, out var alert, out var encoded))
|
||||
if (AlertManager.TryGet(alertType, out var alert))
|
||||
{
|
||||
if (_alerts.TryGetValue(alert.AlertKey, out var alertStateCallback) &&
|
||||
alertStateCallback.AlertState.AlertEncoded == encoded &&
|
||||
alertStateCallback.AlertState.Severity == severity && alertStateCallback.AlertState.Cooldown == cooldown)
|
||||
alert.AlertType == alertType &&
|
||||
alertStateCallback.Severity == severity && alertStateCallback.Cooldown == cooldown)
|
||||
{
|
||||
alertStateCallback.OnClickAlert = onClickAlert;
|
||||
return;
|
||||
}
|
||||
|
||||
_alerts[alert.AlertKey] = new ClickableAlertState
|
||||
{
|
||||
AlertState = new AlertState
|
||||
{Cooldown = cooldown, AlertEncoded = encoded, Severity = severity},
|
||||
OnClickAlert = onClickAlert
|
||||
};
|
||||
_alerts[alert.AlertKey] = new AlertState
|
||||
{Cooldown = cooldown, Severity = severity};
|
||||
|
||||
AfterShowAlert();
|
||||
|
||||
Dirty();
|
||||
|
||||
@@ -212,7 +149,12 @@ namespace Content.Shared.GameObjects.Components.Mobs
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked after clearing an alert prior to dirtying the control
|
||||
/// Invoked after showing an alert prior to dirtying the component
|
||||
/// </summary>
|
||||
protected virtual void AfterShowAlert() { }
|
||||
|
||||
/// <summary>
|
||||
/// Invoked after clearing an alert prior to dirtying the component
|
||||
/// </summary>
|
||||
protected virtual void AfterClearAlert() { }
|
||||
}
|
||||
@@ -220,9 +162,9 @@ namespace Content.Shared.GameObjects.Components.Mobs
|
||||
[Serializable, NetSerializable]
|
||||
public class AlertsComponentState : ComponentState
|
||||
{
|
||||
public AlertState[] Alerts;
|
||||
public Dictionary<AlertKey, AlertState> Alerts;
|
||||
|
||||
public AlertsComponentState(AlertState[] alerts) : base(ContentNetIDs.ALERTS)
|
||||
public AlertsComponentState(Dictionary<AlertKey, AlertState> alerts) : base(ContentNetIDs.ALERTS)
|
||||
{
|
||||
Alerts = alerts;
|
||||
}
|
||||
@@ -234,46 +176,19 @@ namespace Content.Shared.GameObjects.Components.Mobs
|
||||
[Serializable, NetSerializable]
|
||||
public class ClickAlertMessage : ComponentMessage
|
||||
{
|
||||
public readonly byte EncodedAlert;
|
||||
public readonly AlertType AlertType;
|
||||
|
||||
public ClickAlertMessage(byte encodedAlert)
|
||||
public ClickAlertMessage(AlertType alertType)
|
||||
{
|
||||
Directed = true;
|
||||
EncodedAlert = encodedAlert;
|
||||
AlertType = alertType;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public struct AlertState
|
||||
{
|
||||
public byte AlertEncoded;
|
||||
public short? Severity;
|
||||
public ValueTuple<TimeSpan, TimeSpan>? Cooldown;
|
||||
}
|
||||
|
||||
public struct ClickableAlertState
|
||||
{
|
||||
public AlertState AlertState;
|
||||
public OnClickAlert OnClickAlert;
|
||||
}
|
||||
|
||||
public delegate void OnClickAlert(ClickAlertEventArgs args);
|
||||
|
||||
public class ClickAlertEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Player clicking the alert
|
||||
/// </summary>
|
||||
public readonly IEntity Player;
|
||||
/// <summary>
|
||||
/// Alert that was clicked
|
||||
/// </summary>
|
||||
public readonly AlertPrototype Alert;
|
||||
|
||||
public ClickAlertEventArgs(IEntity player, AlertPrototype alert)
|
||||
{
|
||||
Player = player;
|
||||
Alert = alert;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,17 +69,11 @@ namespace Content.Shared.GameObjects.Components.Pulling
|
||||
{
|
||||
case PullStartedMessage msg:
|
||||
Pulling = msg.Pulled.Owner;
|
||||
if (ownerStatus != null)
|
||||
{
|
||||
ownerStatus.ShowAlert(AlertType.Pulling, onClickAlert: OnClickAlert);
|
||||
}
|
||||
ownerStatus?.ShowAlert(AlertType.Pulling);
|
||||
break;
|
||||
case PullStoppedMessage _:
|
||||
Pulling = null;
|
||||
if (ownerStatus != null)
|
||||
{
|
||||
ownerStatus.ClearAlert(AlertType.Pulling);
|
||||
}
|
||||
ownerStatus?.ClearAlert(AlertType.Pulling);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,7 +86,8 @@
|
||||
public const uint SINGULARITY = 1080;
|
||||
public const uint CHARACTERINFO = 1081;
|
||||
public const uint REAGENT_GRINDER = 1082;
|
||||
public const uint DAMAGEABLE = 1083;
|
||||
public const uint ACTIONS = 1083;
|
||||
public const uint DAMAGEABLE = 1084;
|
||||
|
||||
// Net IDs for integration tests.
|
||||
public const uint PREDICTION_TEST = 10001;
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
using Content.Shared.GameObjects.Components.Mobs;
|
||||
using Content.Shared.Interfaces.GameObjects.Components;
|
||||
using Robust.Shared.GameObjects.Systems;
|
||||
|
||||
namespace Content.Shared.GameObjects.EntitySystems
|
||||
{
|
||||
/// <summary>
|
||||
/// Evicts action states with expired cooldowns.
|
||||
/// </summary>
|
||||
public class SharedActionSystem : EntitySystem
|
||||
{
|
||||
private const float CooldownCheckIntervalSeconds = 10;
|
||||
private float _timeSinceCooldownCheck;
|
||||
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
_timeSinceCooldownCheck += frameTime;
|
||||
if (_timeSinceCooldownCheck < CooldownCheckIntervalSeconds) return;
|
||||
|
||||
foreach (var comp in ComponentManager.EntityQuery<SharedActionsComponent>(false))
|
||||
{
|
||||
comp.ExpireCooldowns();
|
||||
}
|
||||
_timeSinceCooldownCheck -= CooldownCheckIntervalSeconds;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,5 +41,16 @@ namespace Content.Shared.Input
|
||||
public static readonly BoundKeyFunction Arcade1 = "Arcade1";
|
||||
public static readonly BoundKeyFunction Arcade2 = "Arcade2";
|
||||
public static readonly BoundKeyFunction Arcade3 = "Arcade3";
|
||||
public static readonly BoundKeyFunction OpenActionsMenu = "OpenAbilitiesMenu";
|
||||
public static readonly BoundKeyFunction Hotbar0 = "Hotbar0";
|
||||
public static readonly BoundKeyFunction Hotbar1 = "Hotbar1";
|
||||
public static readonly BoundKeyFunction Hotbar2 = "Hotbar2";
|
||||
public static readonly BoundKeyFunction Hotbar3 = "Hotbar3";
|
||||
public static readonly BoundKeyFunction Hotbar4 = "Hotbar4";
|
||||
public static readonly BoundKeyFunction Hotbar5 = "Hotbar5";
|
||||
public static readonly BoundKeyFunction Hotbar6 = "Hotbar6";
|
||||
public static readonly BoundKeyFunction Hotbar7 = "Hotbar7";
|
||||
public static readonly BoundKeyFunction Hotbar8 = "Hotbar8";
|
||||
public static readonly BoundKeyFunction Hotbar9 = "Hotbar9";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,11 @@ using Robust.Shared.Interfaces.GameObjects;
|
||||
namespace Content.Shared.Interfaces.GameObjects.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// This interface gives components behavior when being activated in the world when the user
|
||||
/// is in range and has unobstructed access to the target entity (allows inside blockers).
|
||||
/// This interface gives components behavior when being activated (by default,
|
||||
/// this is done via the "E" key) when the user is in range and has unobstructed access to the target entity
|
||||
/// (allows inside blockers). This includes activating an object in the world as well as activating an
|
||||
/// object in inventory. Unlike IUse, this can be performed on entities that aren't in the active hand,
|
||||
/// even when the active hand is currently holding something else.
|
||||
/// </summary>
|
||||
public interface IActivate
|
||||
{
|
||||
|
||||
@@ -7,8 +7,9 @@ using Robust.Shared.Map;
|
||||
namespace Content.Shared.Interfaces.GameObjects.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// This interface gives components a behavior when clicking on another object and no interaction occurs,
|
||||
/// at any range.
|
||||
/// This interface gives components a behavior when their entity is in the active hand, when
|
||||
/// clicking on another object and no interaction occurs, at any range. This includes
|
||||
/// clicking on an object in the world as well as clicking on an object in inventory.
|
||||
/// </summary>
|
||||
public interface IAfterInteract
|
||||
{
|
||||
|
||||
@@ -7,22 +7,35 @@ using Robust.Shared.Interfaces.GameObjects;
|
||||
namespace Content.Shared.Interfaces.GameObjects.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// This interface gives components behavior when their owner is put in an inventory slot.
|
||||
/// This interface gives components behavior when their entity is put in a non-hand inventory slot,
|
||||
/// regardless of where it came from. This includes moving the entity from a hand slot into a non-hand slot
|
||||
/// (which would also fire <see cref="IUnequippedHand"/>).
|
||||
///
|
||||
/// This DOES NOT fire when putting the entity into a hand slot (<see cref="IEquippedHand"/>), nor
|
||||
/// does it fire when putting the entity into held/equipped storage.
|
||||
/// </summary>
|
||||
public interface IEquipped
|
||||
{
|
||||
void Equipped(EquippedEventArgs eventArgs);
|
||||
}
|
||||
|
||||
public class EquippedEventArgs : EventArgs
|
||||
public abstract class UserEventArgs : EventArgs
|
||||
{
|
||||
public EquippedEventArgs(IEntity user, EquipmentSlotDefines.Slots slot)
|
||||
public IEntity User { get; }
|
||||
|
||||
protected UserEventArgs(IEntity user)
|
||||
{
|
||||
User = user;
|
||||
}
|
||||
}
|
||||
|
||||
public class EquippedEventArgs : UserEventArgs
|
||||
{
|
||||
public EquippedEventArgs(IEntity user, EquipmentSlotDefines.Slots slot) : base(user)
|
||||
{
|
||||
Slot = slot;
|
||||
}
|
||||
|
||||
public IEntity User { get; }
|
||||
public EquipmentSlotDefines.Slots Slot { get; }
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using Content.Shared.GameObjects.Components.Inventory;
|
||||
using Content.Shared.GameObjects.Components.Items;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
|
||||
namespace Content.Shared.Interfaces.GameObjects.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// This interface gives components behavior when their entity is put in a hand inventory slot,
|
||||
/// even if it came from another hand slot (which would also fire <see cref="IUnequippedHand"/>).
|
||||
/// This includes moving the entity from a non-hand slot into a hand slot
|
||||
/// (which would also fire <see cref="IUnequipped"/>).
|
||||
/// </summary>
|
||||
public interface IEquippedHand
|
||||
{
|
||||
void EquippedHand(EquippedHandEventArgs eventArgs);
|
||||
}
|
||||
|
||||
public class EquippedHandEventArgs : UserEventArgs
|
||||
{
|
||||
public EquippedHandEventArgs(IEntity user, SharedHand hand) : base(user)
|
||||
{
|
||||
Hand = hand;
|
||||
}
|
||||
|
||||
public SharedHand Hand { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised when putting the entity into a hand slot
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public class EquippedHandMessage : EntitySystemMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// If this message has already been "handled" by a previous system.
|
||||
/// </summary>
|
||||
public bool Handled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Entity that equipped the item.
|
||||
/// </summary>
|
||||
public IEntity User { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Item that was equipped.
|
||||
/// </summary>
|
||||
public IEntity Equipped { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Hand the item is going into.
|
||||
/// </summary>
|
||||
public SharedHand Hand { get; }
|
||||
|
||||
public EquippedHandMessage(IEntity user, IEntity equipped, SharedHand hand)
|
||||
{
|
||||
User = user;
|
||||
Equipped = equipped;
|
||||
Hand = hand;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,9 @@ using Robust.Shared.Map;
|
||||
namespace Content.Shared.Interfaces.GameObjects.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// This interface gives components behavior when being clicked on by a user with an object in their hand
|
||||
/// who is in range and has unobstructed reach of the target entity (allows inside blockers).
|
||||
/// This interface gives components behavior when their entity is clicked on by a user with an object in their hand
|
||||
/// who is in range and has unobstructed reach of the target entity (allows inside blockers). This includes
|
||||
/// clicking on an object in the world as well as clicking on an object in inventory.
|
||||
/// </summary>
|
||||
public interface IInteractUsing
|
||||
{
|
||||
|
||||
@@ -7,22 +7,25 @@ using Robust.Shared.Interfaces.GameObjects;
|
||||
namespace Content.Shared.Interfaces.GameObjects.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// This interface gives components behavior when their owner is removed from an inventory slot.
|
||||
/// This interface gives components behavior when their entity is removed from a non-hand inventory slot,
|
||||
/// regardless of where it's going to. This includes moving the entity from a non-hand slot into a hand slot
|
||||
/// (which would also fire <see cref="IEquippedHand"/>).
|
||||
///
|
||||
/// This DOES NOT fire when removing the entity from a hand slot (<see cref="IUnequippedHand"/>), nor
|
||||
/// does it fire when removing the entity from held/equipped storage.
|
||||
/// </summary>
|
||||
public interface IUnequipped
|
||||
{
|
||||
void Unequipped(UnequippedEventArgs eventArgs);
|
||||
}
|
||||
|
||||
public class UnequippedEventArgs : EventArgs
|
||||
public class UnequippedEventArgs : UserEventArgs
|
||||
{
|
||||
public UnequippedEventArgs(IEntity user, EquipmentSlotDefines.Slots slot)
|
||||
public UnequippedEventArgs(IEntity user, EquipmentSlotDefines.Slots slot) : base(user)
|
||||
{
|
||||
User = user;
|
||||
Slot = slot;
|
||||
}
|
||||
|
||||
public IEntity User { get; }
|
||||
public EquipmentSlotDefines.Slots Slot { get; }
|
||||
}
|
||||
|
||||
@@ -43,19 +46,19 @@ namespace Content.Shared.Interfaces.GameObjects.Components
|
||||
public IEntity User { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Item that was equipped.
|
||||
/// Item that was unequipped.
|
||||
/// </summary>
|
||||
public IEntity Equipped { get; }
|
||||
public IEntity Unequipped { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Slot where the item was removed from.
|
||||
/// </summary>
|
||||
public EquipmentSlotDefines.Slots Slot { get; }
|
||||
|
||||
public UnequippedMessage(IEntity user, IEntity equipped, EquipmentSlotDefines.Slots slot)
|
||||
public UnequippedMessage(IEntity user, IEntity unequipped, EquipmentSlotDefines.Slots slot)
|
||||
{
|
||||
User = user;
|
||||
Equipped = equipped;
|
||||
Unequipped = unequipped;
|
||||
Slot = slot;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
using System;
|
||||
using Content.Shared.GameObjects.Components.Inventory;
|
||||
using Content.Shared.GameObjects.Components.Items;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
|
||||
namespace Content.Shared.Interfaces.GameObjects.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// This interface gives components behavior when their entity is removed from a hand slot,
|
||||
/// even if it is going into another hand slot (which would also fire <see cref="IEquippedHand"/>).
|
||||
/// This includes moving the entity from a hand slot into a non-hand slot (which would also fire <see cref="IEquipped"/>).
|
||||
/// </summary>
|
||||
public interface IUnequippedHand
|
||||
{
|
||||
void UnequippedHand(UnequippedHandEventArgs eventArgs);
|
||||
}
|
||||
|
||||
public class UnequippedHandEventArgs : UserEventArgs
|
||||
{
|
||||
public UnequippedHandEventArgs(IEntity user, SharedHand hand) : base(user)
|
||||
{
|
||||
Hand = hand;
|
||||
}
|
||||
|
||||
public SharedHand Hand { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised when removing the entity from an inventory slot.
|
||||
/// </summary>
|
||||
[PublicAPI]
|
||||
public class UnequippedHandMessage : EntitySystemMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// If this message has already been "handled" by a previous system.
|
||||
/// </summary>
|
||||
public bool Handled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Entity that equipped the item.
|
||||
/// </summary>
|
||||
public IEntity User { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Item that was unequipped.
|
||||
/// </summary>
|
||||
public IEntity Unequipped { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Hand the item is removed from.
|
||||
/// </summary>
|
||||
public SharedHand Hand { get; }
|
||||
|
||||
public UnequippedHandMessage(IEntity user, IEntity unequipped, SharedHand hand)
|
||||
{
|
||||
User = user;
|
||||
Unequipped = unequipped;
|
||||
Hand = hand;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,8 @@ using Robust.Shared.Interfaces.GameObjects;
|
||||
namespace Content.Shared.Interfaces.GameObjects.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// This interface gives components behavior when using the entity in your hands
|
||||
/// This interface gives components behavior when using the entity in your active hand
|
||||
/// (done by clicking the entity in the active hand or pressing the keybind that defaults to Z).
|
||||
/// </summary>
|
||||
public interface IUse
|
||||
{
|
||||
|
||||
28
Content.Shared/Utility/Cooldowns.cs
Normal file
28
Content.Shared/Utility/Cooldowns.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using Robust.Shared.Interfaces.Timing;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
namespace Content.Shared.Utility
|
||||
{
|
||||
/// <summary>
|
||||
/// Utilities for working with cooldowns.
|
||||
/// </summary>
|
||||
public static class Cooldowns
|
||||
{
|
||||
/// <param name="gameTiming">game timing to use, otherwise will resolve using IoCManager.</param>
|
||||
/// <returns>a cooldown interval starting at GameTiming.Curtime and ending at (offset) from CurTime.
|
||||
/// For example, passing TimeSpan.FromSeconds(5) will create an interval
|
||||
/// from now to 5 seconds from now.</returns>
|
||||
public static (TimeSpan start, TimeSpan end) FromNow(TimeSpan offset, IGameTiming gameTiming = null)
|
||||
{
|
||||
var now = (gameTiming ?? IoCManager.Resolve<IGameTiming>()).CurTime;
|
||||
return (now, now + offset);
|
||||
}
|
||||
|
||||
/// <see cref="FromNow"/>
|
||||
public static (TimeSpan start, TimeSpan end) SecondsFromNow(double seconds, IGameTiming gameTiming = null)
|
||||
{
|
||||
return FromNow(TimeSpan.FromSeconds(seconds), gameTiming);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,10 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Content.Server.GameObjects.Components.Mobs;
|
||||
using Content.Shared.Alert;
|
||||
using Content.Shared.GameObjects.Components.Mobs;
|
||||
using Content.Shared.Utility;
|
||||
using NUnit.Framework;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Tests.Server.GameObjects.Components.Mobs
|
||||
@@ -48,23 +44,23 @@ namespace Content.Tests.Server.GameObjects.Components.Mobs
|
||||
var alertsComponent = new ServerAlertsComponent();
|
||||
alertsComponent = IoCManager.InjectDependencies(alertsComponent);
|
||||
|
||||
Assert.That(alertManager.TryGetWithEncoded(AlertType.LowPressure, out var lowpressure, out var lpencoded));
|
||||
Assert.That(alertManager.TryGetWithEncoded(AlertType.HighPressure, out var highpressure, out var hpencoded));
|
||||
Assert.That(alertManager.TryGet(AlertType.LowPressure, out var lowpressure));
|
||||
Assert.That(alertManager.TryGet(AlertType.HighPressure, out var highpressure));
|
||||
|
||||
alertsComponent.ShowAlert(AlertType.LowPressure);
|
||||
var alertState = alertsComponent.GetComponentState() as AlertsComponentState;
|
||||
Assert.NotNull(alertState);
|
||||
Assert.That(alertState.Alerts.Length, Is.EqualTo(1));
|
||||
Assert.That(alertState.Alerts[0], Is.EqualTo(new AlertState{AlertEncoded = lpencoded}));
|
||||
Assert.That(alertState.Alerts.Count, Is.EqualTo(1));
|
||||
Assert.That(alertState.Alerts.ContainsKey(lowpressure.AlertKey));
|
||||
|
||||
alertsComponent.ShowAlert(AlertType.HighPressure);
|
||||
alertState = alertsComponent.GetComponentState() as AlertsComponentState;
|
||||
Assert.That(alertState.Alerts.Length, Is.EqualTo(1));
|
||||
Assert.That(alertState.Alerts[0], Is.EqualTo(new AlertState{AlertEncoded = hpencoded}));
|
||||
Assert.That(alertState.Alerts.Count, Is.EqualTo(1));
|
||||
Assert.That(alertState.Alerts.ContainsKey(highpressure.AlertKey));
|
||||
|
||||
alertsComponent.ClearAlertCategory(AlertCategory.Pressure);
|
||||
alertState = alertsComponent.GetComponentState() as AlertsComponentState;
|
||||
Assert.That(alertState.Alerts.Length, Is.EqualTo(0));
|
||||
Assert.That(alertState.Alerts.Count, Is.EqualTo(0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,25 +37,10 @@ namespace Content.Tests.Shared.Alert
|
||||
Assert.That(alertManager.TryGet(AlertType.HighPressure, out var highPressure));
|
||||
Assert.That(highPressure.IconPath, Is.EqualTo("/Textures/Interface/Alerts/Pressure/highpressure.png"));
|
||||
|
||||
Assert.That(alertManager.TryGetWithEncoded(AlertType.LowPressure, out lowPressure, out var encodedLowPressure));
|
||||
Assert.That(alertManager.TryGet(AlertType.LowPressure, out lowPressure));
|
||||
Assert.That(lowPressure.IconPath, Is.EqualTo("/Textures/Interface/Alerts/Pressure/lowpressure.png"));
|
||||
Assert.That(alertManager.TryGetWithEncoded(AlertType.HighPressure, out highPressure, out var encodedHighPressure));
|
||||
Assert.That(alertManager.TryGet(AlertType.HighPressure, out highPressure));
|
||||
Assert.That(highPressure.IconPath, Is.EqualTo("/Textures/Interface/Alerts/Pressure/highpressure.png"));
|
||||
|
||||
Assert.That(alertManager.TryEncode(lowPressure, out var encodedLowPressure2));
|
||||
Assert.That(encodedLowPressure2, Is.EqualTo(encodedLowPressure));
|
||||
Assert.That(alertManager.TryEncode(highPressure, out var encodedHighPressure2));
|
||||
Assert.That(encodedHighPressure2, Is.EqualTo(encodedHighPressure));
|
||||
Assert.That(encodedLowPressure, Is.Not.EqualTo(encodedHighPressure));
|
||||
|
||||
Assert.That(alertManager.TryDecode(encodedLowPressure, out var decodedLowPressure));
|
||||
Assert.That(decodedLowPressure, Is.EqualTo(lowPressure));
|
||||
Assert.That(alertManager.TryDecode(encodedHighPressure, out var decodedHighPressure));
|
||||
Assert.That(decodedHighPressure, Is.EqualTo(highPressure));
|
||||
|
||||
Assert.False(alertManager.TryEncode(AlertType.Debug1, out _));
|
||||
Assert.False(alertManager.TryGetWithEncoded(AlertType.Debug1, out _, out _));
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
using System.IO;
|
||||
using Content.Server.Utility;
|
||||
using Content.Shared.Alert;
|
||||
using Content.Shared.Interfaces;
|
||||
using NUnit.Framework;
|
||||
using Robust.Shared.Interfaces.Log;
|
||||
using Robust.Shared.Interfaces.Reflection;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.UnitTesting;
|
||||
using YamlDotNet.RepresentationModel;
|
||||
@@ -22,7 +26,6 @@ namespace Content.Tests.Shared.Alert
|
||||
minSeverity: 0
|
||||
maxSeverity: 6";
|
||||
|
||||
|
||||
[Test]
|
||||
public void TestAlertKey()
|
||||
{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user