Actions System + UI (#2710)

Co-authored-by: Vera Aguilera Puerto <6766154+Zumorica@users.noreply.github.com>
This commit is contained in:
chairbender
2020-12-13 14:28:20 -08:00
committed by GitHub
parent fd0df9a00a
commit 7a3c281f60
150 changed files with 7283 additions and 854 deletions

View File

@@ -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);

View File

@@ -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);

View File

@@ -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)
{
}

View File

@@ -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}");

View File

@@ -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
}
}

View File

@@ -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);
}
}
}

View File

@@ -1,93 +0,0 @@
#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.Controls;
using Robust.Shared.Maths;
namespace Content.Client.GameObjects.Components.Mobs
{
public class AlertControl : BaseButton
{
public AlertPrototype Alert { get; }
/// <summary>
/// Total duration of the cooldown in seconds. Null if no duration / cooldown.
/// </summary>
public int? TotalDuration { get; set; }
private short? _severity;
private readonly TextureRect _icon;
private readonly CooldownGraphic _cooldownGraphic;
private readonly IResourceCache _resourceCache;
/// <summary>
/// Creates an alert control reflecting the indicated alert + state
/// </summary>
/// <param name="alert">alert to display</param>
/// <param name="severity">severity of alert, null if alert doesn't have severity levels</param>
/// <param name="resourceCache">resourceCache to use to load alert icon textures</param>
public AlertControl(AlertPrototype alert, short? severity, IResourceCache resourceCache)
{
_resourceCache = resourceCache;
Alert = alert;
_severity = severity;
var texture = _resourceCache.GetTexture(alert.GetIconPath(_severity));
_icon = new TextureRect
{
TextureScale = (2, 2),
Texture = texture
};
Children.Add(_icon);
_cooldownGraphic = new CooldownGraphic();
Children.Add(_cooldownGraphic);
}
/// <summary>
/// Change the alert severity, changing the displayed icon
/// </summary>
public void SetSeverity(short? severity)
{
if (_severity != severity)
{
_severity = severity;
_icon.Texture = _resourceCache.GetTexture(Alert.GetIconPath(_severity));
}
}
/// <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)
{
if (!alertCooldown.HasValue)
{
_cooldownGraphic.Progress = 0;
_cooldownGraphic.Visible = false;
TotalDuration = null;
}
else
{
var start = alertCooldown.Value.Start;
var end = alertCooldown.Value.End;
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;
}
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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;
}
}
}
}

View 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();
}
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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);
}
}
}