Hud refactor (#7202)
Co-authored-by: DrSmugleaf <DrSmugleaf@users.noreply.github.com> Co-authored-by: Jezithyr <jmaster9999@gmail.com> Co-authored-by: Jezithyr <Jezithyr@gmail.com> Co-authored-by: Visne <39844191+Visne@users.noreply.github.com> Co-authored-by: wrexbe <wrexbe@protonmail.com> Co-authored-by: wrexbe <81056464+wrexbe@users.noreply.github.com>
This commit is contained in:
@@ -1,33 +1,17 @@
|
||||
using Content.Client.Actions.Assignments;
|
||||
using Content.Client.Actions.UI;
|
||||
using Content.Client.Construction;
|
||||
using Content.Client.DragDrop;
|
||||
using Content.Client.Hands;
|
||||
using Content.Client.Items.Managers;
|
||||
using Content.Client.Outline;
|
||||
using Content.Client.Popups;
|
||||
using System.IO;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Actions.ActionTypes;
|
||||
using Content.Shared.Input;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Input.Binding;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Serialization.Manager;
|
||||
using Robust.Shared.Serialization.Markdown;
|
||||
using Robust.Shared.Serialization.Markdown.Mapping;
|
||||
using Robust.Shared.Serialization.Markdown.Sequence;
|
||||
using Robust.Shared.Utility;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using YamlDotNet.RepresentationModel;
|
||||
|
||||
namespace Content.Client.Actions
|
||||
@@ -35,268 +19,84 @@ namespace Content.Client.Actions
|
||||
[UsedImplicitly]
|
||||
public sealed class ActionsSystem : SharedActionsSystem
|
||||
{
|
||||
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IUserInterfaceManager _uiManager = default!;
|
||||
[Dependency] private readonly IItemSlotManager _itemSlotManager = default!;
|
||||
[Dependency] private readonly ISerializationManager _serializationManager = default!;
|
||||
[Dependency] private readonly IResourceManager _resourceManager = default!;
|
||||
[Dependency] private readonly IOverlayManager _overlayMan = default!;
|
||||
[Dependency] private readonly PopupSystem _popupSystem = default!;
|
||||
[Dependency] private readonly InteractionOutlineSystem _interactionOutline = default!;
|
||||
[Dependency] private readonly TargetOutlineSystem _targetOutline = default!;
|
||||
[Dependency] private readonly IResourceManager _resources = default!;
|
||||
[Dependency] private readonly ISerializationManager _serialization = default!;
|
||||
|
||||
// TODO Redo assignments, including allowing permanent user configurable slot assignments.
|
||||
/// <summary>
|
||||
/// Current assignments for all hotbars / slots for this entity.
|
||||
/// </summary>
|
||||
public ActionAssignments Assignments = new(Hotbars, Slots);
|
||||
public event Action<ActionType>? OnActionAdded;
|
||||
public event Action<ActionType>? OnActionRemoved;
|
||||
public event Action<ActionsComponent>? OnLinkActions;
|
||||
public event Action? OnUnlinkActions;
|
||||
public event Action? ClearAssignments;
|
||||
public event Action<List<SlotAssignment>>? AssignSlot;
|
||||
|
||||
public const byte Hotbars = 9;
|
||||
public const byte Slots = 10;
|
||||
|
||||
public bool UIDirty;
|
||||
|
||||
public ActionsUI? Ui;
|
||||
private EntityUid? _highlightedEntity;
|
||||
public ActionsComponent? PlayerActions { get; private set; }
|
||||
|
||||
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))
|
||||
.Bind(ContentKeyFunctions.Loadout1,
|
||||
HandleChangeHotbarKeybind(0))
|
||||
.Bind(ContentKeyFunctions.Loadout2,
|
||||
HandleChangeHotbarKeybind(1))
|
||||
.Bind(ContentKeyFunctions.Loadout3,
|
||||
HandleChangeHotbarKeybind(2))
|
||||
.Bind(ContentKeyFunctions.Loadout4,
|
||||
HandleChangeHotbarKeybind(3))
|
||||
.Bind(ContentKeyFunctions.Loadout5,
|
||||
HandleChangeHotbarKeybind(4))
|
||||
.Bind(ContentKeyFunctions.Loadout6,
|
||||
HandleChangeHotbarKeybind(5))
|
||||
.Bind(ContentKeyFunctions.Loadout7,
|
||||
HandleChangeHotbarKeybind(6))
|
||||
.Bind(ContentKeyFunctions.Loadout8,
|
||||
HandleChangeHotbarKeybind(7))
|
||||
.Bind(ContentKeyFunctions.Loadout9,
|
||||
HandleChangeHotbarKeybind(8))
|
||||
// when selecting a target, we intercept clicks in the game world, treating them as our target selection. We want to
|
||||
// take priority before any other systems handle the click.
|
||||
.BindBefore(EngineKeyFunctions.Use, new PointerInputCmdHandler(TargetingOnUse, outsidePrediction: true),
|
||||
typeof(ConstructionSystem), typeof(DragDropSystem))
|
||||
.BindBefore(EngineKeyFunctions.UIRightClick, new PointerInputCmdHandler(TargetingCancel, outsidePrediction: true))
|
||||
.Register<ActionsSystem>();
|
||||
|
||||
SubscribeLocalEvent<ActionsComponent, PlayerAttachedEvent>(OnPlayerAttached);
|
||||
SubscribeLocalEvent<ActionsComponent, PlayerDetachedEvent>(OnPlayerDetached);
|
||||
SubscribeLocalEvent<ActionsComponent, ComponentHandleState>(HandleState);
|
||||
SubscribeLocalEvent<ActionsComponent, ComponentHandleState>(HandleComponentState);
|
||||
}
|
||||
|
||||
public override void Dirty(ActionType action)
|
||||
private void HandleComponentState(EntityUid uid, ActionsComponent component, ref ComponentHandleState args)
|
||||
{
|
||||
// Should only ever receive component states for attached player's component.
|
||||
// --> lets not bother unnecessarily dirtying and prediction-resetting actions for other players.
|
||||
if (action.AttachedEntity != _playerManager.LocalPlayer?.ControlledEntity)
|
||||
if (args.Current is not ActionsComponentState currentState)
|
||||
return;
|
||||
|
||||
base.Dirty(action);
|
||||
UIDirty = true;
|
||||
}
|
||||
List<ActionType> added = new();
|
||||
List<ActionType> removed = new();
|
||||
|
||||
private void HandleState(EntityUid uid, ActionsComponent component, ref ComponentHandleState args)
|
||||
foreach (var actionType in component.Actions)
|
||||
{
|
||||
if (args.Current is not ActionsComponentState state)
|
||||
return;
|
||||
|
||||
var serverActions = new SortedSet<ActionType>(state.Actions);
|
||||
|
||||
foreach (var act in component.Actions.ToList())
|
||||
if (!currentState.Actions.Contains(actionType))
|
||||
{
|
||||
if (act.ClientExclusive)
|
||||
continue;
|
||||
|
||||
if (!serverActions.TryGetValue(act, out var serverAct))
|
||||
{
|
||||
component.Actions.Remove(act);
|
||||
if (act.AutoRemove && !(Ui?.Locked ?? false))
|
||||
Assignments.Remove(act);
|
||||
continue;
|
||||
}
|
||||
|
||||
act.CopyFrom(serverAct);
|
||||
serverActions.Remove(serverAct);
|
||||
}
|
||||
|
||||
// Anything that remains is a new action
|
||||
foreach (var newAct in serverActions)
|
||||
{
|
||||
// We create a new action, not just sorting a reference to the state's action.
|
||||
component.Actions.Add((ActionType) newAct.Clone());
|
||||
}
|
||||
|
||||
UIDirty = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Highlights the item slot (inventory or hand) that contains this item
|
||||
/// </summary>
|
||||
/// <param name="item"></param>
|
||||
public void HighlightItemSlot(EntityUid item)
|
||||
{
|
||||
StopHighlightingItemSlot();
|
||||
|
||||
_highlightedEntity = item;
|
||||
_itemSlotManager.HighlightEntity(item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops highlighting any item slots we are currently highlighting.
|
||||
/// </summary>H
|
||||
public void StopHighlightingItemSlot()
|
||||
{
|
||||
if (_highlightedEntity == null)
|
||||
return;
|
||||
|
||||
_itemSlotManager.UnHighlightEntity(_highlightedEntity.Value);
|
||||
_highlightedEntity = null;
|
||||
}
|
||||
|
||||
protected override void AddActionInternal(ActionsComponent comp, ActionType action)
|
||||
{
|
||||
// Sometimes the client receives actions from the server, before predicting that newly added components will add
|
||||
// their own shared actions. Just in case those systems ever decided to directly access action properties (e.g.,
|
||||
// action.Toggled), we will remove duplicates:
|
||||
if (comp.Actions.TryGetValue(action, out var existing))
|
||||
{
|
||||
comp.Actions.Remove(existing);
|
||||
Assignments.Replace(existing, action);
|
||||
}
|
||||
|
||||
comp.Actions.Add(action);
|
||||
}
|
||||
|
||||
public override void AddAction(EntityUid uid, ActionType action, EntityUid? provider, ActionsComponent? comp = null, bool dirty = true)
|
||||
{
|
||||
if (!Resolve(uid, ref comp, false))
|
||||
return;
|
||||
|
||||
base.AddAction(uid, action, provider, comp, dirty);
|
||||
|
||||
|
||||
if (uid == _playerManager.LocalPlayer?.ControlledEntity)
|
||||
UIDirty = true;
|
||||
}
|
||||
|
||||
public override void RemoveActions(EntityUid uid, IEnumerable<ActionType> actions, ActionsComponent? comp = null, bool dirty = true)
|
||||
{
|
||||
if (uid != _playerManager.LocalPlayer?.ControlledEntity)
|
||||
return;
|
||||
|
||||
if (!Resolve(uid, ref comp, false))
|
||||
return;
|
||||
|
||||
base.RemoveActions(uid, actions, comp, dirty);
|
||||
|
||||
foreach (var act in actions)
|
||||
{
|
||||
if (act.AutoRemove && !(Ui?.Locked ?? false))
|
||||
Assignments.Remove(act);
|
||||
}
|
||||
|
||||
UIDirty = true;
|
||||
}
|
||||
|
||||
public override void FrameUpdate(float frameTime)
|
||||
{
|
||||
// avoid updating GUI when doing predictions & resetting state.
|
||||
if (UIDirty)
|
||||
{
|
||||
UIDirty = false;
|
||||
UpdateUI();
|
||||
removed.Add(actionType);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the displayed hotbar (and menu) based on current state of actions.
|
||||
/// </summary>
|
||||
public void UpdateUI()
|
||||
foreach (var serverAction in currentState.Actions)
|
||||
{
|
||||
if (Ui == null)
|
||||
return;
|
||||
|
||||
foreach (var action in Ui.Component.Actions)
|
||||
if (!component.Actions.TryGetValue(serverAction, out var clientAction))
|
||||
{
|
||||
if (action.AutoPopulate && !Assignments.Assignments.ContainsKey(action))
|
||||
Assignments.AutoPopulate(action, Ui.SelectedHotbar, false);
|
||||
added.Add((ActionType) serverAction.Clone());
|
||||
}
|
||||
else
|
||||
{
|
||||
clientAction.CopyFrom(serverAction);
|
||||
}
|
||||
}
|
||||
|
||||
// get rid of actions that are no longer available to the user
|
||||
foreach (var (action, index) in Assignments.Assignments.ToList())
|
||||
foreach (var actionType in added)
|
||||
{
|
||||
if (index.Count == 0)
|
||||
{
|
||||
Assignments.Assignments.Remove(action);
|
||||
continue;
|
||||
component.Actions.Add(actionType);
|
||||
OnActionAdded?.Invoke(actionType);
|
||||
}
|
||||
|
||||
if (action.AutoRemove && !Ui.Locked && !Ui.Component.Actions.Contains(action))
|
||||
Assignments.ClearSlot(index[0].Hotbar, index[0].Slot, false);
|
||||
}
|
||||
|
||||
Assignments.PreventAutoPopulate.RemoveWhere(action => !Ui.Component.Actions.Contains(action));
|
||||
|
||||
Ui.UpdateUI();
|
||||
}
|
||||
|
||||
public void HandleHotbarKeybind(byte slot, in PointerInputCmdHandler.PointerInputCmdArgs args)
|
||||
foreach (var actionType in removed)
|
||||
{
|
||||
Ui?.HandleHotbarKeybind(slot, args);
|
||||
component.Actions.Remove(actionType);
|
||||
OnActionRemoved?.Invoke(actionType);
|
||||
}
|
||||
|
||||
public void HandleChangeHotbarKeybind(byte hotbar, in PointerInputCmdHandler.PointerInputCmdArgs args)
|
||||
{
|
||||
Ui?.HandleChangeHotbarKeybind(hotbar, args);
|
||||
}
|
||||
|
||||
private void OnPlayerDetached(EntityUid uid, ActionsComponent component, PlayerDetachedEvent args)
|
||||
{
|
||||
if (Ui == null) return;
|
||||
_uiManager.StateRoot.RemoveChild(Ui);
|
||||
Ui = null;
|
||||
}
|
||||
|
||||
private void OnPlayerAttached(EntityUid uid, ActionsComponent component, PlayerAttachedEvent args)
|
||||
{
|
||||
Assignments = new(Hotbars, Slots);
|
||||
Ui = new ActionsUI(this, component);
|
||||
_uiManager.StateRoot.AddChild(Ui);
|
||||
UIDirty = true;
|
||||
if (uid != _playerManager.LocalPlayer?.ControlledEntity)
|
||||
return;
|
||||
|
||||
OnLinkActions?.Invoke(component);
|
||||
PlayerActions = component;
|
||||
}
|
||||
|
||||
private void OnPlayerDetached(EntityUid uid, ActionsComponent component, PlayerDetachedEvent? args = null)
|
||||
{
|
||||
if (uid != _playerManager.LocalPlayer?.ControlledEntity)
|
||||
return;
|
||||
|
||||
OnUnlinkActions?.Invoke();
|
||||
PlayerActions = null;
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
@@ -305,70 +105,25 @@ namespace Content.Client.Actions
|
||||
CommandBinds.Unregister<ActionsSystem>();
|
||||
}
|
||||
|
||||
private PointerInputCmdHandler HandleHotbarKeybind(byte slot)
|
||||
public void TriggerAction(ActionType? action)
|
||||
{
|
||||
// delegate to the ActionsUI, simulating a click on it
|
||||
return new((in PointerInputCmdHandler.PointerInputCmdArgs args) =>
|
||||
{
|
||||
var playerEntity = _playerManager.LocalPlayer?.ControlledEntity;
|
||||
if (playerEntity == null ||
|
||||
!EntityManager.TryGetComponent<ActionsComponent?>(playerEntity.Value, out var actionsComponent)) return false;
|
||||
|
||||
HandleHotbarKeybind(slot, args);
|
||||
return true;
|
||||
}, false);
|
||||
}
|
||||
|
||||
private PointerInputCmdHandler HandleChangeHotbarKeybind(byte hotbar)
|
||||
{
|
||||
// delegate to the ActionsUI, simulating a click on it
|
||||
return new((in PointerInputCmdHandler.PointerInputCmdArgs args) =>
|
||||
{
|
||||
var playerEntity = _playerManager.LocalPlayer?.ControlledEntity;
|
||||
if (!EntityManager.TryGetComponent<ActionsComponent?>(playerEntity, out var actionsComponent)) return false;
|
||||
|
||||
HandleChangeHotbarKeybind(hotbar, args);
|
||||
return true;
|
||||
},
|
||||
false);
|
||||
}
|
||||
|
||||
private void ToggleActionsMenu()
|
||||
{
|
||||
Ui?.ToggleActionsMenu();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A action slot was pressed. This either performs the action or toggles the targeting mode.
|
||||
/// </summary>
|
||||
internal void OnSlotPressed(ActionSlot slot)
|
||||
{
|
||||
if (Ui == null)
|
||||
if (PlayerActions == null || action == null || _playerManager.LocalPlayer?.ControlledEntity is not { Valid: true } user)
|
||||
return;
|
||||
|
||||
if (slot.Action == null || _playerManager.LocalPlayer?.ControlledEntity is not EntityUid user)
|
||||
if (action.Provider != null && Deleted(action.Provider))
|
||||
return;
|
||||
|
||||
if (slot.Action.Provider != null && Deleted(slot.Action.Provider))
|
||||
return;
|
||||
|
||||
if (slot.Action is not InstantAction instantAction)
|
||||
if (action is not InstantAction instantAction)
|
||||
{
|
||||
// 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);
|
||||
return;
|
||||
}
|
||||
|
||||
if (slot.Action.ClientExclusive)
|
||||
if (action.ClientExclusive)
|
||||
{
|
||||
if (instantAction.Event != null)
|
||||
instantAction.Event.Performer = user;
|
||||
|
||||
PerformAction(Ui.Component, instantAction, instantAction.Event, GameTiming.CurTime);
|
||||
PerformAction(PlayerActions, instantAction, instantAction.Event, GameTiming.CurTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -377,226 +132,6 @@ namespace Content.Client.Actions
|
||||
}
|
||||
}
|
||||
|
||||
private bool TargetingCancel(in PointerInputCmdHandler.PointerInputCmdArgs args)
|
||||
{
|
||||
if (!GameTiming.IsFirstTimePredicted)
|
||||
return false;
|
||||
|
||||
// only do something for actual target-based actions
|
||||
if (Ui?.SelectingTargetFor?.Action == null)
|
||||
return false;
|
||||
|
||||
Ui.StopTargeting();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If the user clicked somewhere, and they are currently targeting an action, try and perform it.
|
||||
/// </summary>
|
||||
private bool TargetingOnUse(in PointerInputCmdHandler.PointerInputCmdArgs args)
|
||||
{
|
||||
if (!GameTiming.IsFirstTimePredicted)
|
||||
return false;
|
||||
|
||||
// only do something for actual target-based actions
|
||||
if (Ui?.SelectingTargetFor?.Action is not TargetedAction action)
|
||||
return false;
|
||||
|
||||
if (_playerManager.LocalPlayer?.ControlledEntity is not EntityUid user)
|
||||
return false;
|
||||
|
||||
if (!TryComp(user, out ActionsComponent? comp))
|
||||
return false;
|
||||
|
||||
// Is the action currently valid?
|
||||
if (!action.Enabled
|
||||
|| action.Charges != null && action.Charges == 0
|
||||
|| action.Cooldown.HasValue && action.Cooldown.Value.End > GameTiming.CurTime)
|
||||
{
|
||||
// The user is targeting with this action, but it is not valid. Maybe mark this click as
|
||||
// handled and prevent further interactions.
|
||||
return !action.InteractOnMiss;
|
||||
}
|
||||
|
||||
switch (action)
|
||||
{
|
||||
case WorldTargetAction mapTarget:
|
||||
return TryTargetWorld(args, mapTarget, user, comp) || !action.InteractOnMiss;
|
||||
|
||||
case EntityTargetAction entTarget:
|
||||
return TargetEntity(args, entTarget, user, comp) || !action.InteractOnMiss;
|
||||
|
||||
default:
|
||||
Logger.Error($"Unknown targeting action: {action.GetType()}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryTargetWorld(in PointerInputCmdHandler.PointerInputCmdArgs args, WorldTargetAction action, EntityUid user, ActionsComponent actionComp)
|
||||
{
|
||||
var coords = args.Coordinates.ToMap(EntityManager);
|
||||
|
||||
if (!ValidateWorldTarget(user, coords, action))
|
||||
{
|
||||
// Invalid target.
|
||||
if (action.DeselectOnMiss)
|
||||
Ui?.StopTargeting();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (action.ClientExclusive)
|
||||
{
|
||||
if (action.Event != null)
|
||||
{
|
||||
action.Event.Target = coords;
|
||||
action.Event.Performer = user;
|
||||
}
|
||||
|
||||
PerformAction(actionComp, action, action.Event, GameTiming.CurTime);
|
||||
}
|
||||
else
|
||||
EntityManager.RaisePredictiveEvent(new RequestPerformActionEvent(action, coords));
|
||||
|
||||
if (!action.Repeat)
|
||||
Ui?.StopTargeting();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TargetEntity(in PointerInputCmdHandler.PointerInputCmdArgs args, EntityTargetAction action, EntityUid user, ActionsComponent actionComp)
|
||||
{
|
||||
if (!ValidateEntityTarget(user, args.EntityUid, action))
|
||||
{
|
||||
if (action.DeselectOnMiss)
|
||||
Ui?.StopTargeting();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (action.ClientExclusive)
|
||||
{
|
||||
if (action.Event != null)
|
||||
{
|
||||
action.Event.Target = args.EntityUid;
|
||||
action.Event.Performer = user;
|
||||
}
|
||||
|
||||
PerformAction(actionComp, action, action.Event, GameTiming.CurTime);
|
||||
}
|
||||
else
|
||||
EntityManager.RaisePredictiveEvent(new RequestPerformActionEvent(action, args.EntityUid));
|
||||
|
||||
if (!action.Repeat)
|
||||
Ui?.StopTargeting();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute convenience functionality for actions (pop-ups, sound, speech)
|
||||
/// </summary>
|
||||
protected override bool PerformBasicActions(EntityUid user, ActionType action)
|
||||
{
|
||||
var performedAction = action.Sound != null
|
||||
|| !string.IsNullOrWhiteSpace(action.UserPopup)
|
||||
|| !string.IsNullOrWhiteSpace(action.Popup);
|
||||
|
||||
if (!GameTiming.IsFirstTimePredicted)
|
||||
return performedAction;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(action.UserPopup))
|
||||
{
|
||||
var msg = (!action.Toggled || string.IsNullOrWhiteSpace(action.PopupToggleSuffix))
|
||||
? Loc.GetString(action.UserPopup)
|
||||
: Loc.GetString(action.UserPopup + action.PopupToggleSuffix);
|
||||
|
||||
_popupSystem.PopupEntity(msg, user);
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(action.Popup))
|
||||
{
|
||||
var msg = (!action.Toggled || string.IsNullOrWhiteSpace(action.PopupToggleSuffix))
|
||||
? Loc.GetString(action.Popup)
|
||||
: Loc.GetString(action.Popup + action.PopupToggleSuffix);
|
||||
|
||||
_popupSystem.PopupEntity(msg, user);
|
||||
}
|
||||
|
||||
if (action.Sound != null)
|
||||
SoundSystem.Play(action.Sound.GetSound(), Filter.Local(), user, action.AudioParams);
|
||||
|
||||
return performedAction;
|
||||
}
|
||||
|
||||
internal void StopTargeting()
|
||||
{
|
||||
_targetOutline.Disable();
|
||||
_interactionOutline.SetEnabled(true);
|
||||
|
||||
if (!_overlayMan.TryGetOverlay<ShowHandItemOverlay>(out var handOverlay) || handOverlay == null)
|
||||
return;
|
||||
|
||||
handOverlay.IconOverride = null;
|
||||
handOverlay.EntityOverride = null;
|
||||
}
|
||||
|
||||
internal void StartTargeting(TargetedAction action)
|
||||
{
|
||||
// override "held-item" overlay
|
||||
if (action.TargetingIndicator
|
||||
&& _overlayMan.TryGetOverlay<ShowHandItemOverlay>(out var handOverlay)
|
||||
&& handOverlay != null)
|
||||
{
|
||||
if (action.ItemIconStyle == ItemActionIconStyle.BigItem && action.Provider != null)
|
||||
{
|
||||
handOverlay.EntityOverride = action.Provider;
|
||||
}
|
||||
else if (action.Toggled && action.IconOn != null)
|
||||
handOverlay.IconOverride = action.IconOn.Frame0();
|
||||
else if (action.Icon != null)
|
||||
handOverlay.IconOverride = action.Icon.Frame0();
|
||||
}
|
||||
|
||||
// TODO: allow world-targets to check valid positions. E.g., maybe:
|
||||
// - Draw a red/green ghost entity
|
||||
// - Add a yes/no checkmark where the HandItemOverlay usually is
|
||||
|
||||
// Highlight valid entity targets
|
||||
if (action is not EntityTargetAction entityAction)
|
||||
return;
|
||||
|
||||
Func<EntityUid, bool>? predicate = null;
|
||||
|
||||
if (!entityAction.CanTargetSelf)
|
||||
predicate = e => e != entityAction.AttachedEntity;
|
||||
|
||||
var range = entityAction.CheckCanAccess ? action.Range : -1;
|
||||
|
||||
_interactionOutline.SetEnabled(false);
|
||||
_targetOutline.Enable(range, entityAction.CheckCanAccess, predicate, entityAction.Whitelist, null);
|
||||
}
|
||||
|
||||
internal void TryFillSlot(byte hotbar, byte index)
|
||||
{
|
||||
if (Ui == null)
|
||||
return;
|
||||
|
||||
var fillEvent = new FillActionSlotEvent();
|
||||
RaiseLocalEvent(Ui.Component.Owner, fillEvent, broadcast: true);
|
||||
|
||||
if (fillEvent.Action == null)
|
||||
return;
|
||||
|
||||
fillEvent.Action.ClientExclusive = true;
|
||||
fillEvent.Action.Temporary = true;
|
||||
fillEvent.Action.AutoPopulate = false;
|
||||
|
||||
Ui.Component.Actions.Add(fillEvent.Action);
|
||||
Assignments.AssignSlot(hotbar, index, fillEvent.Action);
|
||||
|
||||
Ui.UpdateUI();
|
||||
}
|
||||
|
||||
/*public void SaveActionAssignments(string path)
|
||||
{
|
||||
|
||||
@@ -624,13 +159,13 @@ namespace Content.Client.Actions
|
||||
/// </summary>
|
||||
public void LoadActionAssignments(string path, bool userData)
|
||||
{
|
||||
if (Ui == null)
|
||||
if (PlayerActions == null)
|
||||
return;
|
||||
|
||||
var file = new ResourcePath(path).ToRootedPath();
|
||||
TextReader reader = userData
|
||||
? _resourceManager.UserData.OpenText(file)
|
||||
: _resourceManager.ContentFileReadText(file);
|
||||
? _resources.UserData.OpenText(file)
|
||||
: _resources.ContentFileReadText(file);
|
||||
|
||||
var yamlStream = new YamlStream();
|
||||
yamlStream.Load(reader);
|
||||
@@ -638,13 +173,9 @@ namespace Content.Client.Actions
|
||||
if (yamlStream.Documents[0].RootNode.ToDataNode() is not SequenceDataNode sequence)
|
||||
return;
|
||||
|
||||
foreach (var (action, assigns) in Assignments.Assignments)
|
||||
{
|
||||
foreach (var index in assigns)
|
||||
{
|
||||
Assignments.ClearSlot(index.Hotbar, index.Slot, true);
|
||||
}
|
||||
}
|
||||
ClearAssignments?.Invoke();
|
||||
|
||||
var assignments = new List<SlotAssignment>();
|
||||
|
||||
foreach (var entry in sequence.Sequence)
|
||||
{
|
||||
@@ -654,28 +185,33 @@ namespace Content.Client.Actions
|
||||
if (!map.TryGet("action", out var actionNode))
|
||||
continue;
|
||||
|
||||
var action = _serializationManager.Read<ActionType>(actionNode);
|
||||
var action = _serialization.Read<ActionType>(actionNode);
|
||||
|
||||
if (Ui.Component.Actions.TryGetValue(action, out var existingAction))
|
||||
if (PlayerActions.Actions.TryGetValue(action, out var existingAction))
|
||||
{
|
||||
existingAction.CopyFrom(action);
|
||||
action = existingAction;
|
||||
}
|
||||
else
|
||||
Ui.Component.Actions.Add(action);
|
||||
{
|
||||
PlayerActions.Actions.Add(action);
|
||||
}
|
||||
|
||||
if (!map.TryGet("assignments", out var assignmentNode))
|
||||
continue;
|
||||
|
||||
var assignments = _serializationManager.Read<List<(byte Hotbar, byte Slot)>>(assignmentNode);
|
||||
var nodeAssignments = _serialization.Read<List<(byte Hotbar, byte Slot)>>(assignmentNode);
|
||||
|
||||
foreach (var index in assignments)
|
||||
foreach (var index in nodeAssignments)
|
||||
{
|
||||
Assignments.AssignSlot(index.Hotbar, index.Slot, action);
|
||||
var assignment = new SlotAssignment(index.Hotbar, index.Slot, action);
|
||||
assignments.Add(assignment);
|
||||
}
|
||||
}
|
||||
|
||||
UIDirty = true;
|
||||
}
|
||||
AssignSlot?.Invoke(assignments);
|
||||
}
|
||||
|
||||
public record struct SlotAssignment(byte Hotbar, byte Slot, ActionType Action);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,167 +0,0 @@
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Actions.ActionTypes;
|
||||
using System.Linq;
|
||||
|
||||
namespace Content.Client.Actions.Assignments
|
||||
{
|
||||
/// <summary>
|
||||
/// Tracks and manages the hotbar assignments for actions.
|
||||
/// </summary>
|
||||
[DataDefinition]
|
||||
public sealed 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 ActionType?[,] _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>
|
||||
[DataField("assignments")]
|
||||
public readonly Dictionary<ActionType, List<(byte Hotbar, byte Slot)>> Assignments = new();
|
||||
|
||||
/// <summary>
|
||||
/// Actions which have been manually cleared by the user, thus should not
|
||||
/// auto-populate.
|
||||
/// </summary>
|
||||
public readonly SortedSet<ActionType> PreventAutoPopulate = new();
|
||||
|
||||
private readonly byte _numHotbars;
|
||||
private readonly byte _numSlots;
|
||||
|
||||
public ActionAssignments(byte numHotbars, byte numSlots)
|
||||
{
|
||||
_numHotbars = numHotbars;
|
||||
_numSlots = numSlots;
|
||||
_slots = new ActionType?[numHotbars, numSlots];
|
||||
}
|
||||
|
||||
public bool Remove(ActionType action) => Replace(action, null);
|
||||
|
||||
internal bool Replace(ActionType action, ActionType? newAction)
|
||||
{
|
||||
if (!Assignments.Remove(action, out var assigns))
|
||||
return false;
|
||||
|
||||
if (newAction != null)
|
||||
Assignments[newAction] = assigns;
|
||||
|
||||
foreach (var (bar, slot) in assigns)
|
||||
{
|
||||
_slots[bar, slot] = newAction;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <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, ActionType 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 == null)
|
||||
return;
|
||||
|
||||
if (preventAutoPopulate)
|
||||
PreventAutoPopulate.Add(currentAction);
|
||||
|
||||
var assignmentList = Assignments[currentAction];
|
||||
assignmentList = assignmentList.Where(a => a.Hotbar != hotbar || a.Slot != slot).ToList();
|
||||
if (!assignmentList.Any())
|
||||
{
|
||||
Assignments.Remove(currentAction);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assignments[currentAction] = 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(ActionType toAssign, byte currentHotbar, bool force = true)
|
||||
{
|
||||
if (!force && PreventAutoPopulate.Contains(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 != null)
|
||||
continue;
|
||||
|
||||
AssignSlot(hotbar, slot, toAssign);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// there was no empty slot
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the assignment to the indicated slot if there is one.
|
||||
/// </summary>
|
||||
public ActionType? 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(ActionType assignment)
|
||||
{
|
||||
return Assignments.ContainsKey(assignment);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,426 +0,0 @@
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Content.Client.DragDrop;
|
||||
using Content.Client.HUD;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Actions.ActionTypes;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Timing;
|
||||
using static Robust.Client.UserInterface.Controls.BaseButton;
|
||||
using static Robust.Client.UserInterface.Controls.BoxContainer;
|
||||
|
||||
namespace Content.Client.Actions.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Action selection menu, allows filtering and searching over all possible
|
||||
/// actions and populating those actions into the hotbar.
|
||||
/// </summary>
|
||||
public sealed class ActionMenu : DefaultWindow
|
||||
{
|
||||
// Pre-defined global filters that can be used to select actions based on their properties (as opposed to their
|
||||
// own yaml-defined filters).
|
||||
// TODO LOC STRINGs
|
||||
private const string AllFilter = "all";
|
||||
private const string ItemFilter = "item";
|
||||
private const string InnateFilter = "innate";
|
||||
private const string EnabledFilter = "enabled";
|
||||
private const string InstantFilter = "instant";
|
||||
private const string TargetedFilter = "targeted";
|
||||
|
||||
private readonly string[] _filters =
|
||||
{
|
||||
AllFilter,
|
||||
ItemFilter,
|
||||
InnateFilter,
|
||||
EnabledFilter,
|
||||
InstantFilter,
|
||||
TargetedFilter
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
/// <summary>
|
||||
/// Is an action currently being dragged from this window?
|
||||
/// </summary>
|
||||
public bool IsDragging => _dragDropHelper.IsDragging;
|
||||
|
||||
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 IGameHud _gameHud;
|
||||
private readonly DragDropHelper<ActionMenuItem> _dragDropHelper;
|
||||
private readonly IEntityManager _entMan;
|
||||
|
||||
public ActionMenu(ActionsUI actionsUI)
|
||||
{
|
||||
_actionsUI = actionsUI;
|
||||
_gameHud = IoCManager.Resolve<IGameHud>();
|
||||
_entMan = IoCManager.Resolve<IEntityManager>();
|
||||
|
||||
Title = Loc.GetString("ui-actionmenu-title");
|
||||
MinSize = (320, 300);
|
||||
|
||||
Contents.AddChild(new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Vertical,
|
||||
Children =
|
||||
{
|
||||
new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Horizontal,
|
||||
Children =
|
||||
{
|
||||
(_searchBar = new LineEdit
|
||||
{
|
||||
StyleClasses = { StyleNano.StyleClassActionSearchBox },
|
||||
HorizontalExpand = true,
|
||||
PlaceHolder = Loc.GetString("ui-actionmenu-search-bar-placeholder-text")
|
||||
}),
|
||||
(_filterButton = new MultiselectOptionButton<string>()
|
||||
{
|
||||
Label = Loc.GetString("ui-actionmenu-filter-button")
|
||||
})
|
||||
}
|
||||
},
|
||||
(_clearButton = new Button
|
||||
{
|
||||
Text = Loc.GetString("ui-actionmenu-clear-button"),
|
||||
}),
|
||||
(_filterLabel = new Label()),
|
||||
new ScrollContainer
|
||||
{
|
||||
//TODO: needed? MinSize = new Vector2(200.0f, 0.0f),
|
||||
VerticalExpand = true,
|
||||
HorizontalExpand = true,
|
||||
Children =
|
||||
{
|
||||
(_resultsGrid = new GridContainer
|
||||
{
|
||||
MaxGridWidth = 300
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
foreach (var tag in _filters)
|
||||
{
|
||||
_filterButton.AddItem( CultureInfo.CurrentCulture.TextInfo.ToTitleCase(tag), tag);
|
||||
}
|
||||
|
||||
// default to showing all actions.
|
||||
_filterButton.SelectKey(AllFilter);
|
||||
|
||||
UpdateFilterLabel();
|
||||
|
||||
_dragShadow = new TextureRect
|
||||
{
|
||||
MinSize = (64, 64),
|
||||
Stretch = TextureRect.StretchMode.Scale,
|
||||
Visible = false,
|
||||
SetSize = (64, 64)
|
||||
};
|
||||
UserInterfaceManager.PopupRoot.AddChild(_dragShadow);
|
||||
|
||||
_dragDropHelper = new DragDropHelper<ActionMenuItem>(OnBeginActionDrag, OnContinueActionDrag, OnEndActionDrag);
|
||||
}
|
||||
|
||||
protected override void EnteredTree()
|
||||
{
|
||||
base.EnteredTree();
|
||||
_clearButton.OnPressed += OnClearButtonPressed;
|
||||
_searchBar.OnTextChanged += OnSearchTextChanged;
|
||||
_filterButton.OnItemSelected += OnFilterItemSelected;
|
||||
_gameHud.ActionsButtonDown = true;
|
||||
}
|
||||
|
||||
protected override void ExitedTree()
|
||||
{
|
||||
base.ExitedTree();
|
||||
_dragDropHelper.EndDrag();
|
||||
_clearButton.OnPressed -= OnClearButtonPressed;
|
||||
_searchBar.OnTextChanged -= OnSearchTextChanged;
|
||||
_filterButton.OnItemSelected -= OnFilterItemSelected;
|
||||
_gameHud.ActionsButtonDown = false;
|
||||
foreach (var actionMenuControl in _resultsGrid.Children)
|
||||
{
|
||||
var actionMenuItem = (ActionMenuItem) actionMenuControl;
|
||||
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.MaxGridWidth = 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.Position - (32, 32));
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool OnContinueActionDrag(float frameTime)
|
||||
{
|
||||
// keep dragged entity centered under mouse
|
||||
LayoutContainer.SetPosition(_dragShadow, UserInterfaceManager.MousePositionScaled.Position - (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(ButtonEventArgs args)
|
||||
{
|
||||
if (args.Event.Function != EngineKeyFunctions.UIClick ||
|
||||
args.Button is not ActionMenuItem action)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_dragDropHelper.MouseDown(action);
|
||||
}
|
||||
|
||||
private void OnItemButtonUp(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;
|
||||
}
|
||||
|
||||
_actionsUI.System.Assignments.AssignSlot(_actionsUI.SelectedHotbar, targetSlot.SlotIndex, _dragDropHelper.Dragged.Action);
|
||||
_actionsUI.UpdateUI();
|
||||
}
|
||||
|
||||
_dragDropHelper.EndDrag();
|
||||
}
|
||||
|
||||
private void OnItemFocusExited(ActionMenuItem item)
|
||||
{
|
||||
// lost focus, cancel the drag if one is in progress
|
||||
_dragDropHelper.EndDrag();
|
||||
}
|
||||
|
||||
private void OnItemPressed(ButtonEventArgs args)
|
||||
{
|
||||
if (args.Button is not ActionMenuItem actionMenuItem) return;
|
||||
|
||||
_actionsUI.System.Assignments.AutoPopulate(actionMenuItem.Action, _actionsUI.SelectedHotbar);
|
||||
_actionsUI.UpdateUI();
|
||||
}
|
||||
|
||||
private void OnClearButtonPressed(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 = _actionsUI.Component.Actions
|
||||
.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("ui-actionmenu-filter-label",
|
||||
("selectedLabels", string.Join(", ", _filterButton.SelectedLabels)));
|
||||
}
|
||||
}
|
||||
|
||||
private bool MatchesSearchCriteria(ActionType 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(action.DisplayName.ToString()).Contains(standardizedSearch))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// search by provider name
|
||||
if (action.Provider == null || action.Provider == _actionsUI.Component.Owner)
|
||||
return false;
|
||||
|
||||
var name = _entMan.GetComponent<MetaDataComponent>(action.Provider.Value).EntityName;
|
||||
return Standardize(name).Contains(standardizedSearch);
|
||||
}
|
||||
|
||||
private bool ActionMatchesFilterTag(ActionType action, string tag)
|
||||
{
|
||||
return tag switch
|
||||
{
|
||||
EnabledFilter => action.Enabled,
|
||||
ItemFilter => action.Provider != null && action.Provider != _actionsUI.Component.Owner,
|
||||
InnateFilter => action.Provider == null || action.Provider == _actionsUI.Component.Owner,
|
||||
InstantFilter => action is InstantAction,
|
||||
TargetedFilter => action is TargetedAction,
|
||||
_ => true
|
||||
};
|
||||
}
|
||||
|
||||
/// <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 ??= string.Empty;
|
||||
|
||||
// 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<ActionType> actions)
|
||||
{
|
||||
ClearList();
|
||||
|
||||
foreach (var action in actions)
|
||||
{
|
||||
var actionItem = new ActionMenuItem(_actionsUI, action, OnItemFocusExited);
|
||||
_resultsGrid.Children.Add(actionItem);
|
||||
actionItem.SetActionState(action.Enabled);
|
||||
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();
|
||||
}
|
||||
|
||||
/// <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(actionMenuItem.Action.Enabled);
|
||||
}
|
||||
|
||||
SearchAndDisplay();
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
_dragDropHelper.Update(args.DeltaSeconds);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,228 +0,0 @@
|
||||
using System;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Actions.ActionTypes;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.Utility;
|
||||
using static Robust.Client.UserInterface.Controls.BoxContainer;
|
||||
|
||||
namespace Content.Client.Actions.UI
|
||||
{
|
||||
// TODO merge this with action-slot when it gets XAMLd
|
||||
// this has way too much overlap, especially now that they both have the item-sprite icons.
|
||||
|
||||
/// <summary>
|
||||
/// An individual action visible in the action menu.
|
||||
/// </summary>
|
||||
public sealed class ActionMenuItem : ContainerButton
|
||||
{
|
||||
// shorter than default tooltip delay so user can
|
||||
// quickly explore what each action is
|
||||
private const float CustomTooltipDelay = 0.2f;
|
||||
|
||||
private readonly TextureRect _bigActionIcon;
|
||||
private readonly TextureRect _smallActionIcon;
|
||||
private readonly SpriteView _smallItemSpriteView;
|
||||
private readonly SpriteView _bigItemSpriteView;
|
||||
|
||||
public ActionType Action;
|
||||
|
||||
private Action<ActionMenuItem> _onControlFocusExited;
|
||||
|
||||
private readonly ActionsUI _actionsUI;
|
||||
|
||||
public ActionMenuItem(ActionsUI actionsUI, ActionType action, Action<ActionMenuItem> onControlFocusExited)
|
||||
{
|
||||
_actionsUI = actionsUI;
|
||||
Action = action;
|
||||
_onControlFocusExited = onControlFocusExited;
|
||||
|
||||
SetSize = (64, 64);
|
||||
VerticalAlignment = VAlignment.Top;
|
||||
|
||||
_bigActionIcon = new TextureRect
|
||||
{
|
||||
HorizontalExpand = true,
|
||||
VerticalExpand = true,
|
||||
Stretch = TextureRect.StretchMode.Scale,
|
||||
Visible = false
|
||||
};
|
||||
_bigItemSpriteView = new SpriteView
|
||||
{
|
||||
HorizontalExpand = true,
|
||||
VerticalExpand = true,
|
||||
Scale = (2, 2),
|
||||
Visible = false,
|
||||
OverrideDirection = Direction.South,
|
||||
};
|
||||
_smallActionIcon = new TextureRect
|
||||
{
|
||||
HorizontalAlignment = HAlignment.Right,
|
||||
VerticalAlignment = VAlignment.Bottom,
|
||||
Stretch = TextureRect.StretchMode.Scale,
|
||||
Visible = false
|
||||
};
|
||||
_smallItemSpriteView = new SpriteView
|
||||
{
|
||||
HorizontalAlignment = HAlignment.Right,
|
||||
VerticalAlignment = VAlignment.Bottom,
|
||||
Visible = false,
|
||||
OverrideDirection = Direction.South,
|
||||
};
|
||||
|
||||
// padding to the left of the small icon
|
||||
var paddingBoxItemIcon = new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Horizontal,
|
||||
HorizontalExpand = true,
|
||||
VerticalExpand = true,
|
||||
MinSize = (64, 64)
|
||||
};
|
||||
paddingBoxItemIcon.AddChild(new Control()
|
||||
{
|
||||
MinSize = (32, 32),
|
||||
});
|
||||
paddingBoxItemIcon.AddChild(new Control
|
||||
{
|
||||
Children =
|
||||
{
|
||||
_smallActionIcon,
|
||||
_smallItemSpriteView
|
||||
}
|
||||
});
|
||||
AddChild(_bigActionIcon);
|
||||
AddChild(_bigItemSpriteView);
|
||||
AddChild(paddingBoxItemIcon);
|
||||
|
||||
TooltipDelay = CustomTooltipDelay;
|
||||
TooltipSupplier = SupplyTooltip;
|
||||
UpdateIcons();
|
||||
}
|
||||
|
||||
|
||||
public void UpdateIcons()
|
||||
{
|
||||
UpdateItemIcon();
|
||||
|
||||
if (Action == null)
|
||||
{
|
||||
SetActionIcon(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if ((_actionsUI.SelectingTargetFor?.Action == Action || Action.Toggled) && Action.IconOn != null)
|
||||
SetActionIcon(Action.IconOn.Frame0());
|
||||
else
|
||||
SetActionIcon(Action.Icon?.Frame0());
|
||||
}
|
||||
|
||||
private void SetActionIcon(Texture? texture)
|
||||
{
|
||||
if (texture == null || Action == null)
|
||||
{
|
||||
_bigActionIcon.Texture = null;
|
||||
_bigActionIcon.Visible = false;
|
||||
_smallActionIcon.Texture = null;
|
||||
_smallActionIcon.Visible = false;
|
||||
}
|
||||
else if (Action.EntityIcon != null && Action.ItemIconStyle == ItemActionIconStyle.BigItem)
|
||||
{
|
||||
_smallActionIcon.Texture = texture;
|
||||
_smallActionIcon.Modulate = Action.IconColor;
|
||||
_smallActionIcon.Visible = true;
|
||||
_bigActionIcon.Texture = null;
|
||||
_bigActionIcon.Visible = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
_bigActionIcon.Texture = texture;
|
||||
_bigActionIcon.Modulate = Action.IconColor;
|
||||
_bigActionIcon.Visible = true;
|
||||
_smallActionIcon.Texture = null;
|
||||
_smallActionIcon.Visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateItemIcon()
|
||||
{
|
||||
if (Action?.EntityIcon == null || !IoCManager.Resolve<IEntityManager>().TryGetComponent(Action.EntityIcon.Value, out SpriteComponent? sprite))
|
||||
{
|
||||
_bigItemSpriteView.Visible = false;
|
||||
_bigItemSpriteView.Sprite = null;
|
||||
_smallItemSpriteView.Visible = false;
|
||||
_smallItemSpriteView.Sprite = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (Action.ItemIconStyle)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ControlFocusExited()
|
||||
{
|
||||
base.ControlFocusExited();
|
||||
_onControlFocusExited.Invoke(this);
|
||||
}
|
||||
|
||||
private Control SupplyTooltip(Control? sender)
|
||||
{
|
||||
var name = FormattedMessage.FromMarkupPermissive(Loc.GetString(Action.DisplayName));
|
||||
var decr = FormattedMessage.FromMarkupPermissive(Loc.GetString(Action.Description));
|
||||
|
||||
var tooltip = new ActionAlertTooltip(name, decr);
|
||||
|
||||
if (Action.Enabled && (Action.Charges == null || Action.Charges != 0))
|
||||
tooltip.Cooldown = Action.Cooldown;
|
||||
|
||||
return tooltip;
|
||||
}
|
||||
|
||||
/// <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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,551 +0,0 @@
|
||||
using System;
|
||||
using Content.Client.Cooldown;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Actions.ActionTypes;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using static Robust.Client.UserInterface.Controls.BoxContainer;
|
||||
|
||||
namespace Content.Client.Actions.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// A slot in the action hotbar. Not extending BaseButton because
|
||||
/// its needs diverged too much.
|
||||
/// </summary>
|
||||
public sealed 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";
|
||||
|
||||
private bool _spriteViewDirty = false;
|
||||
|
||||
/// <summary>
|
||||
/// Current action in this slot.
|
||||
/// </summary>
|
||||
public ActionType? Action { get; private set; }
|
||||
|
||||
/// <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; }
|
||||
|
||||
private readonly IGameTiming _gameTiming;
|
||||
private readonly IEntityManager _entMan;
|
||||
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 ActionMenu _actionMenu;
|
||||
// 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, ActionMenu actionMenu, byte slotIndex, IGameTiming timing, IEntityManager entMan)
|
||||
{
|
||||
_actionsUI = actionsUI;
|
||||
_actionMenu = actionMenu;
|
||||
_gameTiming = timing;
|
||||
_entMan = entMan;
|
||||
SlotIndex = slotIndex;
|
||||
MouseFilter = MouseFilterMode.Stop;
|
||||
|
||||
SetSize = (64, 64);
|
||||
VerticalAlignment = VAlignment.Top;
|
||||
TooltipDelay = CustomTooltipDelay;
|
||||
TooltipSupplier = SupplyTooltip;
|
||||
|
||||
_number = new RichTextLabel
|
||||
{
|
||||
StyleClasses = {StyleNano.StyleClassHotbarSlotNumber}
|
||||
};
|
||||
_number.SetMessage(SlotNumberLabel());
|
||||
|
||||
_bigActionIcon = new TextureRect
|
||||
{
|
||||
HorizontalExpand = true,
|
||||
VerticalExpand = true,
|
||||
Stretch = TextureRect.StretchMode.Scale,
|
||||
Visible = false
|
||||
};
|
||||
_bigItemSpriteView = new SpriteView
|
||||
{
|
||||
HorizontalExpand = true,
|
||||
VerticalExpand = true,
|
||||
Scale = (2,2),
|
||||
Visible = false,
|
||||
OverrideDirection = Direction.South,
|
||||
};
|
||||
_smallActionIcon = new TextureRect
|
||||
{
|
||||
HorizontalAlignment = HAlignment.Right,
|
||||
VerticalAlignment = VAlignment.Bottom,
|
||||
Stretch = TextureRect.StretchMode.Scale,
|
||||
Visible = false
|
||||
};
|
||||
_smallItemSpriteView = new SpriteView
|
||||
{
|
||||
HorizontalAlignment = HAlignment.Right,
|
||||
VerticalAlignment = VAlignment.Bottom,
|
||||
Visible = false,
|
||||
OverrideDirection = Direction.South,
|
||||
};
|
||||
|
||||
_cooldownGraphic = new CooldownGraphic {Progress = 0, Visible = false};
|
||||
|
||||
// padding to the left of the number to shift it right
|
||||
var paddingBox = new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Horizontal,
|
||||
HorizontalExpand = true,
|
||||
VerticalExpand = true,
|
||||
MinSize = (64, 64)
|
||||
};
|
||||
paddingBox.AddChild(new Control()
|
||||
{
|
||||
MinSize = (4, 4),
|
||||
});
|
||||
paddingBox.AddChild(_number);
|
||||
|
||||
// padding to the left of the small icon
|
||||
var paddingBoxItemIcon = new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Horizontal,
|
||||
HorizontalExpand = true,
|
||||
VerticalExpand = true,
|
||||
MinSize = (64, 64)
|
||||
};
|
||||
paddingBoxItemIcon.AddChild(new Control()
|
||||
{
|
||||
MinSize = (32, 32),
|
||||
});
|
||||
paddingBoxItemIcon.AddChild(new Control
|
||||
{
|
||||
Children =
|
||||
{
|
||||
_smallActionIcon,
|
||||
_smallItemSpriteView
|
||||
}
|
||||
});
|
||||
AddChild(_bigActionIcon);
|
||||
AddChild(_bigItemSpriteView);
|
||||
AddChild(_cooldownGraphic);
|
||||
AddChild(paddingBox);
|
||||
AddChild(paddingBoxItemIcon);
|
||||
DrawModeChanged();
|
||||
}
|
||||
|
||||
private Control? SupplyTooltip(Control sender)
|
||||
{
|
||||
if (Action == null)
|
||||
return null;
|
||||
|
||||
string? extra = null;
|
||||
if (Action.Charges != null)
|
||||
{
|
||||
extra = Loc.GetString("ui-actionslot-charges", ("charges", Action.Charges));
|
||||
}
|
||||
|
||||
var name = FormattedMessage.FromMarkupPermissive(Loc.GetString(Action.DisplayName));
|
||||
var decr = FormattedMessage.FromMarkupPermissive(Loc.GetString(Action.Description));
|
||||
|
||||
var tooltip = new ActionAlertTooltip(name, decr, extra);
|
||||
|
||||
if (Action.Enabled && (Action.Charges == null || Action.Charges != 0))
|
||||
tooltip.Cooldown = Action.Cooldown;
|
||||
|
||||
return tooltip;
|
||||
}
|
||||
|
||||
protected override void MouseEntered()
|
||||
{
|
||||
base.MouseEntered();
|
||||
|
||||
_beingHovered = true;
|
||||
DrawModeChanged();
|
||||
|
||||
if (Action?.Provider != null)
|
||||
_actionsUI.System.HighlightItemSlot(Action.Provider.Value);
|
||||
}
|
||||
|
||||
protected override void MouseExited()
|
||||
{
|
||||
base.MouseExited();
|
||||
_beingHovered = false;
|
||||
CancelPress();
|
||||
DrawModeChanged();
|
||||
_actionsUI.System.StopHighlightingItemSlot();
|
||||
}
|
||||
|
||||
protected override void KeyBindDown(GUIBoundKeyEventArgs args)
|
||||
{
|
||||
base.KeyBindDown(args);
|
||||
|
||||
if (Action == null)
|
||||
{
|
||||
// No action for this slot. Maybe the user is trying to add a mapping action?
|
||||
_actionsUI.System.TryFillSlot(_actionsUI.SelectedHotbar, SlotIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
// only handle clicks, and can't do anything to this if no assignment
|
||||
if (args.Function == EngineKeyFunctions.UIClick)
|
||||
{
|
||||
// might turn into a drag or a full press if released
|
||||
Depress(true);
|
||||
_actionsUI.DragDropHelper.MouseDown(this);
|
||||
DrawModeChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Function != EngineKeyFunctions.UIRightClick || _actionsUI.Locked)
|
||||
return;
|
||||
|
||||
if (_actionsUI.DragDropHelper.IsDragging || _actionMenu.IsDragging)
|
||||
return;
|
||||
|
||||
// user right clicked on an action slot, so we clear it.
|
||||
_actionsUI.System.Assignments.ClearSlot(_actionsUI.SelectedHotbar, SlotIndex, true);
|
||||
|
||||
// If this was a temporary action, and it is no longer assigned to any slots, then we remove the action
|
||||
// altogether.
|
||||
if (Action.Temporary)
|
||||
{
|
||||
// Theres probably a better way to do this.....
|
||||
DebugTools.Assert(Action.ClientExclusive, "Temporary-actions must be client exclusive");
|
||||
|
||||
if (!_actionsUI.System.Assignments.Assignments.TryGetValue(Action, out var index)
|
||||
|| index.Count == 0)
|
||||
{
|
||||
_actionsUI.Component.Actions.Remove(Action);
|
||||
}
|
||||
}
|
||||
|
||||
_actionsUI.StopTargeting();
|
||||
_actionsUI.UpdateUI();
|
||||
}
|
||||
|
||||
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 = _actionsUI.System.Assignments[_actionsUI.SelectedHotbar, fromIdx];
|
||||
var toIdx = targetSlot.SlotIndex;
|
||||
var toAssignment = _actionsUI.System.Assignments[_actionsUI.SelectedHotbar, toIdx];
|
||||
|
||||
if (fromIdx == toIdx) return;
|
||||
if (fromAssignment == null) return;
|
||||
|
||||
_actionsUI.System.Assignments.AssignSlot(_actionsUI.SelectedHotbar, toIdx, fromAssignment);
|
||||
if (toAssignment != null)
|
||||
{
|
||||
_actionsUI.System.Assignments.AssignSlot(_actionsUI.SelectedHotbar, fromIdx, toAssignment);
|
||||
}
|
||||
else
|
||||
{
|
||||
_actionsUI.System.Assignments.ClearSlot(_actionsUI.SelectedHotbar, fromIdx, false);
|
||||
}
|
||||
_actionsUI.UpdateUI();
|
||||
}
|
||||
else
|
||||
{
|
||||
// perform the action
|
||||
if (UserInterfaceManager.CurrentlyHovered == this)
|
||||
{
|
||||
Depress(false);
|
||||
}
|
||||
}
|
||||
_actionsUI.DragDropHelper.EndDrag();
|
||||
DrawModeChanged();
|
||||
}
|
||||
|
||||
protected override void ControlFocusExited()
|
||||
{
|
||||
// lost focus for some reason, cancel the drag if there is one.
|
||||
base.ControlFocusExited();
|
||||
_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.
|
||||
/// </summary>
|
||||
public void Depress(bool depress)
|
||||
{
|
||||
// action can still be toggled if it's allowed to stay selected
|
||||
if (Action == null || !Action.Enabled) return;
|
||||
|
||||
if (_depressed && !depress)
|
||||
{
|
||||
// fire the action
|
||||
_actionsUI.System.OnSlotPressed(this);
|
||||
}
|
||||
_depressed = depress;
|
||||
}
|
||||
|
||||
/// <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>
|
||||
public void Assign(ActionType action)
|
||||
{
|
||||
// already assigned
|
||||
if (Action != null && Action == action) return;
|
||||
|
||||
Action = action;
|
||||
HideTooltip();
|
||||
UpdateIcons();
|
||||
DrawModeChanged();
|
||||
_number.SetMessage(SlotNumberLabel());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the action assigned to this slot
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
if (Action == null) return;
|
||||
Action = null;
|
||||
_depressed = false;
|
||||
HideTooltip();
|
||||
UpdateIcons();
|
||||
DrawModeChanged();
|
||||
_number.SetMessage(SlotNumberLabel());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Display the action in this slot (if there is one) as enabled
|
||||
/// </summary>
|
||||
public void Enable()
|
||||
{
|
||||
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 Disable()
|
||||
{
|
||||
_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 = (Action == null || Action.Enabled) ? EnabledColor : DisabledColor;
|
||||
return FormattedMessage.FromMarkup("[color=" + color + "]" + number + "[/color]");
|
||||
}
|
||||
|
||||
public void UpdateIcons()
|
||||
{
|
||||
UpdateItemIcon();
|
||||
|
||||
if (Action == null)
|
||||
{
|
||||
SetActionIcon(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if ((_actionsUI.SelectingTargetFor?.Action == Action || Action.Toggled) && Action.IconOn != null)
|
||||
SetActionIcon(Action.IconOn.Frame0());
|
||||
else
|
||||
SetActionIcon(Action.Icon?.Frame0());
|
||||
}
|
||||
|
||||
private void SetActionIcon(Texture? texture)
|
||||
{
|
||||
if (texture == null || Action == null)
|
||||
{
|
||||
_bigActionIcon.Texture = null;
|
||||
_bigActionIcon.Visible = false;
|
||||
_smallActionIcon.Texture = null;
|
||||
_smallActionIcon.Visible = false;
|
||||
}
|
||||
else if (Action.EntityIcon != null && Action.ItemIconStyle == ItemActionIconStyle.BigItem)
|
||||
{
|
||||
_smallActionIcon.Texture = texture;
|
||||
_smallActionIcon.Modulate = Action.IconColor;
|
||||
_smallActionIcon.Visible = true;
|
||||
_bigActionIcon.Texture = null;
|
||||
_bigActionIcon.Visible = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
_bigActionIcon.Texture = texture;
|
||||
_bigActionIcon.Modulate = Action.IconColor;
|
||||
_bigActionIcon.Visible = true;
|
||||
_smallActionIcon.Texture = null;
|
||||
_smallActionIcon.Visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateItemIcon()
|
||||
{
|
||||
if (Action?.EntityIcon != null && !_entMan.EntityExists(Action.EntityIcon))
|
||||
{
|
||||
// This is almost certainly because a player received/processed their own actions component state before
|
||||
// being send the entity in their inventory that enabled this action.
|
||||
|
||||
// Defer updating icons to the next FrameUpdate().
|
||||
_spriteViewDirty = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (Action?.EntityIcon == null || !_entMan.TryGetComponent(Action.EntityIcon.Value, out SpriteComponent? sprite))
|
||||
{
|
||||
_bigItemSpriteView.Visible = false;
|
||||
_bigItemSpriteView.Sprite = null;
|
||||
_smallItemSpriteView.Visible = false;
|
||||
_smallItemSpriteView.Sprite = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (Action.ItemIconStyle)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void DrawModeChanged()
|
||||
{
|
||||
// always show the normal empty button style if no action in this slot
|
||||
if (Action == null)
|
||||
{
|
||||
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassNormal);
|
||||
return;
|
||||
}
|
||||
|
||||
// show a hover only if the action is usable or another action is being dragged on top of this
|
||||
if (_beingHovered && (_actionsUI.DragDropHelper.IsDragging || _actionMenu.IsDragging || Action.Enabled))
|
||||
{
|
||||
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassHover);
|
||||
}
|
||||
|
||||
// it's only depress-able if it's usable, so if we're depressed
|
||||
// show the depressed style
|
||||
if (_depressed)
|
||||
{
|
||||
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassPressed);
|
||||
return;
|
||||
}
|
||||
|
||||
// if it's toggled on, always show the toggled on style (currently same as depressed style)
|
||||
if (Action.Toggled || _actionsUI.SelectingTargetFor == this)
|
||||
{
|
||||
// when there's a toggle sprite, we're showing that sprite instead of highlighting this slot
|
||||
SetOnlyStylePseudoClass(Action.IconOn != null ? ContainerButton.StylePseudoClassNormal :
|
||||
ContainerButton.StylePseudoClassPressed);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Action.Enabled)
|
||||
{
|
||||
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassDisabled);
|
||||
return;
|
||||
}
|
||||
|
||||
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassNormal);
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
|
||||
if (_spriteViewDirty)
|
||||
{
|
||||
_spriteViewDirty = false;
|
||||
UpdateIcons();
|
||||
}
|
||||
|
||||
if (Action == null || Action.Cooldown == null || !Action.Enabled)
|
||||
{
|
||||
_cooldownGraphic.Visible = false;
|
||||
_cooldownGraphic.Progress = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
var cooldown = Action.Cooldown.Value;
|
||||
var duration = cooldown.End - cooldown.Start;
|
||||
var curTime = _gameTiming.CurTime;
|
||||
var length = duration.TotalSeconds;
|
||||
var progress = (curTime - cooldown.Start).TotalSeconds / length;
|
||||
var ratio = (progress <= 1 ? (1 - progress) : (curTime - cooldown.End).TotalSeconds * -5);
|
||||
|
||||
_cooldownGraphic.Progress = MathHelper.Clamp((float)ratio, -1, 1);
|
||||
if (ratio > -1f)
|
||||
_cooldownGraphic.Visible = true;
|
||||
else
|
||||
{
|
||||
_cooldownGraphic.Visible = false;
|
||||
Action.Cooldown = null;
|
||||
DrawModeChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,505 +0,0 @@
|
||||
using Content.Client.DragDrop;
|
||||
using Content.Client.HUD;
|
||||
using Content.Client.Resources;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Actions.ActionTypes;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Input.Binding;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using static Robust.Client.UserInterface.Controls.BoxContainer;
|
||||
|
||||
namespace Content.Client.Actions.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// The action hotbar on the left side of the screen.
|
||||
/// </summary>
|
||||
public sealed class ActionsUI : Container
|
||||
{
|
||||
private const float DragDeadZone = 10f;
|
||||
private const float CustomTooltipDelay = 0.4f;
|
||||
internal readonly ActionsSystem System;
|
||||
private readonly IGameHud _gameHud;
|
||||
private readonly IEntityManager _entMan;
|
||||
private readonly IGameTiming _timing;
|
||||
|
||||
/// <summary>
|
||||
/// The action component of the currently attached entity.
|
||||
/// </summary>
|
||||
public readonly ActionsComponent Component;
|
||||
|
||||
private readonly ActionSlot[] _slots;
|
||||
|
||||
private readonly GridContainer _slotContainer;
|
||||
|
||||
private readonly TextureButton _lockButton;
|
||||
private readonly TextureButton _settingsButton;
|
||||
private readonly Label _loadoutNumber;
|
||||
private readonly Texture _lockTexture;
|
||||
private readonly Texture _unlockTexture;
|
||||
private readonly BoxContainer _loadoutContainer;
|
||||
|
||||
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(ActionsSystem system, ActionsComponent component)
|
||||
{
|
||||
SetValue(LayoutContainer.DebugProperty, true);
|
||||
System = system;
|
||||
Component = component;
|
||||
_gameHud = IoCManager.Resolve<IGameHud>();
|
||||
_timing = IoCManager.Resolve<IGameTiming>();
|
||||
_entMan = IoCManager.Resolve<IEntityManager>();
|
||||
_menu = new ActionMenu(this);
|
||||
|
||||
LayoutContainer.SetGrowHorizontal(this, LayoutContainer.GrowDirection.End);
|
||||
LayoutContainer.SetGrowVertical(this, LayoutContainer.GrowDirection.Constrain);
|
||||
LayoutContainer.SetAnchorTop(this, 0f);
|
||||
LayoutContainer.SetAnchorBottom(this, 0.8f);
|
||||
LayoutContainer.SetMarginLeft(this, 13);
|
||||
LayoutContainer.SetMarginTop(this, 110);
|
||||
|
||||
HorizontalAlignment = HAlignment.Left;
|
||||
VerticalExpand = true;
|
||||
|
||||
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},
|
||||
HorizontalAlignment = HAlignment.Left,
|
||||
VerticalAlignment = VAlignment.Top
|
||||
};
|
||||
AddChild(panelContainer);
|
||||
|
||||
var hotbarContainer = new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Vertical,
|
||||
SeparationOverride = 3,
|
||||
HorizontalAlignment = HAlignment.Left
|
||||
};
|
||||
panelContainer.AddChild(hotbarContainer);
|
||||
|
||||
var settingsContainer = new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Horizontal,
|
||||
HorizontalExpand = true
|
||||
};
|
||||
hotbarContainer.AddChild(settingsContainer);
|
||||
|
||||
settingsContainer.AddChild(new Control { HorizontalExpand = true, SizeFlagsStretchRatio = 1 });
|
||||
_lockTexture = resourceCache.GetTexture("/Textures/Interface/Nano/lock.svg.192dpi.png");
|
||||
_unlockTexture = resourceCache.GetTexture("/Textures/Interface/Nano/lock_open.svg.192dpi.png");
|
||||
_lockButton = new TextureButton
|
||||
{
|
||||
TextureNormal = _unlockTexture,
|
||||
HorizontalAlignment = HAlignment.Center,
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
SizeFlagsStretchRatio = 1,
|
||||
Scale = (0.5f, 0.5f),
|
||||
ToolTip = Loc.GetString("ui-actionsui-function-lock-action-slots"),
|
||||
TooltipDelay = CustomTooltipDelay
|
||||
};
|
||||
settingsContainer.AddChild(_lockButton);
|
||||
settingsContainer.AddChild(new Control { HorizontalExpand = true, SizeFlagsStretchRatio = 2 });
|
||||
_settingsButton = new TextureButton
|
||||
{
|
||||
TextureNormal = resourceCache.GetTexture("/Textures/Interface/Nano/gear.svg.192dpi.png"),
|
||||
HorizontalAlignment = HAlignment.Center,
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
SizeFlagsStretchRatio = 1,
|
||||
Scale = (0.5f, 0.5f),
|
||||
ToolTip = Loc.GetString("ui-actionsui-function-open-abilities-menu"),
|
||||
TooltipDelay = CustomTooltipDelay
|
||||
};
|
||||
settingsContainer.AddChild(_settingsButton);
|
||||
settingsContainer.AddChild(new Control { HorizontalExpand = true, SizeFlagsStretchRatio = 1 });
|
||||
|
||||
// this allows a 2 column layout if window gets too small
|
||||
_slotContainer = new GridContainer
|
||||
{
|
||||
MaxGridHeight = CalcMaxHeight()
|
||||
};
|
||||
hotbarContainer.AddChild(_slotContainer);
|
||||
|
||||
_loadoutContainer = new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Horizontal,
|
||||
HorizontalExpand = true,
|
||||
MouseFilter = MouseFilterMode.Stop
|
||||
};
|
||||
hotbarContainer.AddChild(_loadoutContainer);
|
||||
|
||||
_loadoutContainer.AddChild(new Control { HorizontalExpand = true, SizeFlagsStretchRatio = 1 });
|
||||
var previousHotbarIcon = new TextureRect()
|
||||
{
|
||||
Texture = resourceCache.GetTexture("/Textures/Interface/Nano/left_arrow.svg.192dpi.png"),
|
||||
HorizontalAlignment = HAlignment.Center,
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
SizeFlagsStretchRatio = 1,
|
||||
TextureScale = (0.5f, 0.5f)
|
||||
};
|
||||
_loadoutContainer.AddChild(previousHotbarIcon);
|
||||
_loadoutContainer.AddChild(new Control { HorizontalExpand = true, SizeFlagsStretchRatio = 2 });
|
||||
_loadoutNumber = new Label
|
||||
{
|
||||
Text = "1",
|
||||
SizeFlagsStretchRatio = 1
|
||||
};
|
||||
_loadoutContainer.AddChild(_loadoutNumber);
|
||||
_loadoutContainer.AddChild(new Control { HorizontalExpand = true, SizeFlagsStretchRatio = 2 });
|
||||
var nextHotbarIcon = new TextureRect
|
||||
{
|
||||
Texture = resourceCache.GetTexture("/Textures/Interface/Nano/right_arrow.svg.192dpi.png"),
|
||||
HorizontalAlignment = HAlignment.Center,
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
SizeFlagsStretchRatio = 1,
|
||||
TextureScale = (0.5f, 0.5f)
|
||||
};
|
||||
_loadoutContainer.AddChild(nextHotbarIcon);
|
||||
_loadoutContainer.AddChild(new Control { HorizontalExpand = true, SizeFlagsStretchRatio = 1 });
|
||||
|
||||
_slots = new ActionSlot[ActionsSystem.Slots];
|
||||
|
||||
_dragShadow = new TextureRect
|
||||
{
|
||||
MinSize = (64, 64),
|
||||
Stretch = TextureRect.StretchMode.Scale,
|
||||
Visible = false,
|
||||
SetSize = (64, 64)
|
||||
};
|
||||
UserInterfaceManager.PopupRoot.AddChild(_dragShadow);
|
||||
|
||||
for (byte i = 0; i < ActionsSystem.Slots; i++)
|
||||
{
|
||||
var slot = new ActionSlot(this, _menu, i, _timing, _entMan);
|
||||
_slotContainer.AddChild(slot);
|
||||
_slots[i] = slot;
|
||||
}
|
||||
|
||||
DragDropHelper = new DragDropHelper<ActionSlot>(OnBeginActionDrag, OnContinueActionDrag, OnEndActionDrag);
|
||||
DragDropHelper.Deadzone = DragDeadZone;
|
||||
|
||||
MinSize = (10, 400);
|
||||
}
|
||||
|
||||
protected override void EnteredTree()
|
||||
{
|
||||
base.EnteredTree();
|
||||
_lockButton.OnPressed += OnLockPressed;
|
||||
_settingsButton.OnPressed += OnToggleActionsMenu;
|
||||
_loadoutContainer.OnKeyBindDown += OnHotbarPaginate;
|
||||
_gameHud.ActionsButtonToggled += OnToggleActionsMenuTopButton;
|
||||
_gameHud.ActionsButtonDown = false;
|
||||
_gameHud.ActionsButtonVisible = true;
|
||||
}
|
||||
|
||||
protected override void ExitedTree()
|
||||
{
|
||||
base.ExitedTree();
|
||||
StopTargeting();
|
||||
_menu.Close();
|
||||
_lockButton.OnPressed -= OnLockPressed;
|
||||
_settingsButton.OnPressed -= OnToggleActionsMenu;
|
||||
_loadoutContainer.OnKeyBindDown -= OnHotbarPaginate;
|
||||
_gameHud.ActionsButtonToggled -= OnToggleActionsMenuTopButton;
|
||||
_gameHud.ActionsButtonDown = false;
|
||||
_gameHud.ActionsButtonVisible = false;
|
||||
}
|
||||
|
||||
protected override void Resized()
|
||||
{
|
||||
base.Resized();
|
||||
_slotContainer.MaxGridHeight = 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.MaxGridHeight = 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 action = System.Assignments[SelectedHotbar, actionSlot.SlotIndex];
|
||||
|
||||
if (action == null)
|
||||
{
|
||||
if (SelectingTargetFor == actionSlot)
|
||||
StopTargeting(true);
|
||||
actionSlot.Clear();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Component.Actions.TryGetValue(action, out var actualAction))
|
||||
{
|
||||
UpdateActionSlot(actualAction, actionSlot);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Action not in the actions component, but in the assignment list.
|
||||
// This is either an action that doesn't auto-clear from the menu, or the action menu was locked.
|
||||
// Show the old action, but make sure it is disabled;
|
||||
action.Enabled = false;
|
||||
action.Toggled = false;
|
||||
|
||||
// If we enable the item-sprite, and if the item-sprite has a visual toggle, then the player will be
|
||||
// able to know whether the item is toggled, even if it is not in their LOS (but in PVS). And for things
|
||||
// like PDA sprites, the player can even see whether the action's item is currently inside of their PVS.
|
||||
// SO unless theres some way of "freezing" a sprite-view, we just have to disable it.
|
||||
action.ItemIconStyle = ItemActionIconStyle.NoItem;
|
||||
|
||||
UpdateActionSlot(action, actionSlot);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateActionSlot(ActionType action, ActionSlot actionSlot)
|
||||
{
|
||||
actionSlot.Assign(action);
|
||||
|
||||
if (!action.Enabled)
|
||||
{
|
||||
// just revoked an action we were trying to target with, stop targeting
|
||||
if (SelectingTargetFor?.Action != null && SelectingTargetFor.Action == action)
|
||||
{
|
||||
StopTargeting();
|
||||
}
|
||||
|
||||
actionSlot.Disable();
|
||||
}
|
||||
else
|
||||
{
|
||||
actionSlot.Enable();
|
||||
}
|
||||
|
||||
actionSlot.UpdateIcons();
|
||||
actionSlot.DrawModeChanged();
|
||||
}
|
||||
|
||||
private void OnHotbarPaginate(GUIBoundKeyEventArgs args)
|
||||
{
|
||||
// rather than clicking the arrows themselves, the user can click the hbox so it's more
|
||||
// "forgiving" for misclicks, and we simply check which side they are closer to
|
||||
if (args.Function != EngineKeyFunctions.UIClick) return;
|
||||
|
||||
var rightness = args.RelativePosition.X / _loadoutContainer.Width;
|
||||
if (rightness > 0.5)
|
||||
{
|
||||
ChangeHotbar((byte) ((SelectedHotbar + 1) % ActionsSystem.Hotbars));
|
||||
}
|
||||
else
|
||||
{
|
||||
var newBar = SelectedHotbar == 0 ? ActionsSystem.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 (actionSlot.Action == null)
|
||||
return;
|
||||
|
||||
// If we were targeting something else we should stop
|
||||
StopTargeting();
|
||||
|
||||
SelectingTargetFor = actionSlot;
|
||||
|
||||
if (actionSlot.Action is TargetedAction targetAction)
|
||||
System.StartTargeting(targetAction);
|
||||
|
||||
UpdateUI();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Switch out of targeting mode if currently selecting target for an action
|
||||
/// </summary>
|
||||
public void StopTargeting(bool updating = false)
|
||||
{
|
||||
if (SelectingTargetFor == null)
|
||||
return;
|
||||
|
||||
SelectingTargetFor = null;
|
||||
System.StopTargeting();
|
||||
|
||||
// Sometimes targeting gets stopped mid-UI update.
|
||||
// in that case, don't need to do a nested UI refresh.
|
||||
if (!updating)
|
||||
UpdateUI();
|
||||
}
|
||||
|
||||
private void OnToggleActionsMenu(BaseButton.ButtonEventArgs args)
|
||||
{
|
||||
ToggleActionsMenu();
|
||||
}
|
||||
|
||||
private void OnToggleActionsMenuTopButton(bool open)
|
||||
{
|
||||
if (open == _menu.IsOpen) return;
|
||||
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.Position - (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.Position - (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);
|
||||
actionSlot.DrawModeChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle hotbar change.
|
||||
/// </summary>
|
||||
/// <param name="hotbar">hotbar index to switch to</param>
|
||||
public void HandleChangeHotbarKeybind(byte hotbar, PointerInputCmdHandler.PointerInputCmdArgs args)
|
||||
{
|
||||
ChangeHotbar(hotbar);
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
DragDropHelper.Update(args.DeltaSeconds);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
using Content.Client.Administration.Managers;
|
||||
using Content.Client.Administration.UI;
|
||||
using Content.Client.Administration.UI.Tabs.ObjectsTab;
|
||||
using Content.Client.Administration.UI.Tabs.PlayerTab;
|
||||
using Content.Client.HUD;
|
||||
using Content.Client.Verbs;
|
||||
using Content.Shared.Input;
|
||||
using Robust.Client.Console;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Input.Binding;
|
||||
using Robust.Shared.Network;
|
||||
|
||||
namespace Content.Client.Administration.Systems
|
||||
{
|
||||
public sealed partial class AdminSystem
|
||||
{
|
||||
[Dependency] private readonly INetManager _netManager = default!;
|
||||
[Dependency] private readonly IInputManager _inputManager = default!;
|
||||
[Dependency] private readonly IGameHud _gameHud = default!;
|
||||
[Dependency] private readonly IClientAdminManager _clientAdminManager = default!;
|
||||
[Dependency] private readonly IClientConGroupController _clientConGroupController = default!;
|
||||
[Dependency] private readonly IClientConsoleHost _clientConsoleHost = default!;
|
||||
|
||||
[Dependency] private readonly VerbSystem _verbSystem = default!;
|
||||
|
||||
private AdminMenuWindow? _window;
|
||||
private readonly List<BaseWindow> _commandWindows = new();
|
||||
|
||||
private void InitializeMenu()
|
||||
{
|
||||
// Reset the AdminMenu Window on disconnect
|
||||
_netManager.Disconnect += (_, _) => ResetWindow();
|
||||
|
||||
_inputManager.SetInputCommand(ContentKeyFunctions.OpenAdminMenu,
|
||||
InputCmdHandler.FromDelegate(_ => Toggle()));
|
||||
|
||||
_clientAdminManager.AdminStatusUpdated += () =>
|
||||
{
|
||||
// when status changes, show the top button if we can open admin menu.
|
||||
// if we can't or we lost admin status, close it and hide the button.
|
||||
_gameHud.AdminButtonVisible = CanOpen();
|
||||
if (!_gameHud.AdminButtonVisible)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
};
|
||||
_gameHud.AdminButtonToggled += (open) =>
|
||||
{
|
||||
if (open)
|
||||
{
|
||||
TryOpen();
|
||||
}
|
||||
else
|
||||
{
|
||||
Close();
|
||||
}
|
||||
};
|
||||
_gameHud.AdminButtonVisible = CanOpen();
|
||||
_gameHud.AdminButtonDown = false;
|
||||
}
|
||||
|
||||
|
||||
public void ResetWindow()
|
||||
{
|
||||
_window?.Close();
|
||||
_window?.Dispose();
|
||||
_window = null;
|
||||
|
||||
foreach (var window in _commandWindows)
|
||||
{
|
||||
window.Close();
|
||||
window.Dispose();
|
||||
}
|
||||
|
||||
_commandWindows.Clear();
|
||||
}
|
||||
|
||||
public void OpenCommand(BaseWindow window)
|
||||
{
|
||||
_commandWindows.Add(window);
|
||||
window.OpenCentered();
|
||||
}
|
||||
|
||||
public void Open()
|
||||
{
|
||||
if (_window == null)
|
||||
{
|
||||
_window = new AdminMenuWindow();
|
||||
_window.OnClose += Close;
|
||||
}
|
||||
|
||||
_window.PlayerTabControl.OnEntryPressed += PlayerTabEntryPressed;
|
||||
_window.ObjectsTabControl.OnEntryPressed += ObjectsTabEntryPressed;
|
||||
_window.OpenCentered();
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
if (_window != null)
|
||||
{
|
||||
_window.PlayerTabControl.OnEntryPressed -= PlayerTabEntryPressed;
|
||||
_window.ObjectsTabControl.OnEntryPressed -= ObjectsTabEntryPressed;
|
||||
}
|
||||
|
||||
_window?.Close();
|
||||
|
||||
foreach (var window in _commandWindows)
|
||||
window?.Dispose();
|
||||
_commandWindows.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the player can open the window
|
||||
/// </summary>
|
||||
/// <returns>True if the player is allowed</returns>
|
||||
public bool CanOpen()
|
||||
{
|
||||
return _clientConGroupController.CanAdminMenu();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the player can open the window and tries to open it
|
||||
/// </summary>
|
||||
public void TryOpen()
|
||||
{
|
||||
if (CanOpen())
|
||||
Open();
|
||||
}
|
||||
|
||||
public void Toggle()
|
||||
{
|
||||
if (_window != null && _window.IsOpen)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
else
|
||||
{
|
||||
TryOpen();
|
||||
}
|
||||
}
|
||||
|
||||
private void PlayerTabEntryPressed(BaseButton.ButtonEventArgs args)
|
||||
{
|
||||
if (args.Button is not PlayerTabEntry button
|
||||
|| button.PlayerUid == null)
|
||||
return;
|
||||
|
||||
var uid = button.PlayerUid.Value;
|
||||
var function = args.Event.Function;
|
||||
|
||||
if (function == EngineKeyFunctions.UIClick)
|
||||
_clientConsoleHost.ExecuteCommand($"vv {uid}");
|
||||
else if (function == EngineKeyFunctions.UseSecondary)
|
||||
_verbSystem.VerbMenu.OpenVerbMenu(uid, true);
|
||||
else
|
||||
return;
|
||||
|
||||
args.Event.Handle();
|
||||
}
|
||||
|
||||
private void ObjectsTabEntryPressed(BaseButton.ButtonEventArgs args)
|
||||
{
|
||||
if (args.Button is not ObjectsTabEntry button)
|
||||
return;
|
||||
|
||||
var uid = button.AssocEntity;
|
||||
var function = args.Event.Function;
|
||||
|
||||
if (function == EngineKeyFunctions.UIClick)
|
||||
_clientConsoleHost.ExecuteCommand($"vv {uid}");
|
||||
else if (function == EngineKeyFunctions.UseSecondary)
|
||||
_verbSystem.VerbMenu.OpenVerbMenu(uid, true);
|
||||
else
|
||||
return;
|
||||
|
||||
args.Event.Handle();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,6 @@ namespace Content.Client.Administration.Systems
|
||||
base.Initialize();
|
||||
|
||||
InitializeOverlay();
|
||||
InitializeMenu();
|
||||
SubscribeNetworkEvent<FullPlayerListEvent>(OnPlayerListChanged);
|
||||
SubscribeNetworkEvent<PlayerInfoChangedEvent>(OnPlayerInfoChanged);
|
||||
SubscribeNetworkEvent<RoundRestartCleanupEvent>(OnRoundRestartCleanup);
|
||||
|
||||
@@ -1,150 +1,18 @@
|
||||
#nullable enable
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Client.Administration.Managers;
|
||||
using Content.Client.Administration.UI;
|
||||
using Content.Client.Administration.UI.CustomControls;
|
||||
using Content.Client.HUD;
|
||||
using Content.Shared.Administration;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Client.Administration.Systems
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public sealed class BwoinkSystem : SharedBwoinkSystem
|
||||
{
|
||||
[Dependency] private readonly IClientAdminManager _adminManager = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IClyde _clyde = default!;
|
||||
[Dependency] private readonly IGameHud _hud = default!;
|
||||
|
||||
private BwoinkWindow? _adminWindow;
|
||||
private DefaultWindow? _plainWindow;
|
||||
private readonly Dictionary<NetUserId, BwoinkPanel> _activePanelMap = new();
|
||||
|
||||
public bool IsOpen => (_adminWindow?.IsOpen ?? false) || (_plainWindow?.IsOpen ?? false);
|
||||
|
||||
public event Action? AdminReceivedAHelp;
|
||||
public event Action? AdminOpenedAHelp;
|
||||
public event EventHandler<BwoinkTextMessage>? OnBwoinkTextMessageRecieved;
|
||||
|
||||
protected override void OnBwoinkTextMessage(BwoinkTextMessage message, EntitySessionEventArgs eventArgs)
|
||||
{
|
||||
base.OnBwoinkTextMessage(message, eventArgs);
|
||||
LogBwoink(message);
|
||||
// Actual line
|
||||
var window = EnsurePanel(message.UserId);
|
||||
window.ReceiveLine(message);
|
||||
// Play a sound if we didn't send it
|
||||
var localPlayer = _playerManager.LocalPlayer;
|
||||
if (localPlayer?.UserId != message.TrueSender)
|
||||
{
|
||||
SoundSystem.Play("/Audio/Effects/adminhelp.ogg", Filter.Local());
|
||||
_clyde.RequestWindowAttention();
|
||||
}
|
||||
|
||||
// If they're not an admin force it open so they read
|
||||
// If it's admin-admin messaging then eh.
|
||||
if (!_adminManager.HasFlag(AdminFlags.Adminhelp))
|
||||
_plainWindow?.Open();
|
||||
else
|
||||
{
|
||||
_adminWindow?.OnBwoink(message.UserId);
|
||||
|
||||
if (_adminWindow?.IsOpen == true)
|
||||
return;
|
||||
AdminReceivedAHelp?.Invoke();
|
||||
_hud.SetInfoRed(true);
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryGetChannel(NetUserId ch, [NotNullWhen(true)] out BwoinkPanel? bp) => _activePanelMap.TryGetValue(ch, out bp);
|
||||
|
||||
private BwoinkPanel EnsureAdmin(NetUserId channelId)
|
||||
{
|
||||
_adminWindow ??= new BwoinkWindow(this);
|
||||
|
||||
if (!_activePanelMap.TryGetValue(channelId, out var existingPanel))
|
||||
{
|
||||
_activePanelMap[channelId] = existingPanel = new BwoinkPanel(this, channelId);
|
||||
existingPanel.Visible = false;
|
||||
if (!_adminWindow.BwoinkArea.Children.Contains(existingPanel))
|
||||
_adminWindow.BwoinkArea.AddChild(existingPanel);
|
||||
}
|
||||
|
||||
return existingPanel;
|
||||
}
|
||||
|
||||
private BwoinkPanel EnsurePlain(NetUserId channelId)
|
||||
{
|
||||
BwoinkPanel bp;
|
||||
if (_plainWindow is null)
|
||||
{
|
||||
bp = new BwoinkPanel(this, channelId);
|
||||
_plainWindow = new DefaultWindow()
|
||||
{
|
||||
TitleClass="windowTitleAlert",
|
||||
HeaderClass="windowHeaderAlert",
|
||||
Title=Loc.GetString("bwoink-user-title"),
|
||||
SetSize=(400, 200),
|
||||
};
|
||||
|
||||
_plainWindow.Contents.AddChild(bp);
|
||||
}
|
||||
else
|
||||
{
|
||||
bp = (BwoinkPanel) _plainWindow.Contents.GetChild(0);
|
||||
}
|
||||
|
||||
return bp;
|
||||
}
|
||||
|
||||
public BwoinkPanel EnsurePanel(NetUserId channelId)
|
||||
{
|
||||
if (_adminManager.HasFlag(AdminFlags.Adminhelp))
|
||||
return EnsureAdmin(channelId);
|
||||
|
||||
return EnsurePlain(channelId);
|
||||
}
|
||||
|
||||
public void Open(NetUserId? channelId = null)
|
||||
{
|
||||
if (channelId == null)
|
||||
{
|
||||
var localPlayer = _playerManager.LocalPlayer;
|
||||
if (localPlayer != null)
|
||||
Open(localPlayer.UserId);
|
||||
return;
|
||||
}
|
||||
|
||||
_hud.SetInfoRed(false);
|
||||
AdminOpenedAHelp?.Invoke();
|
||||
|
||||
if (_adminManager.HasFlag(AdminFlags.Adminhelp))
|
||||
{
|
||||
SelectChannel(channelId.Value);
|
||||
_adminWindow?.Open();
|
||||
return;
|
||||
}
|
||||
|
||||
EnsurePlain(channelId.Value);
|
||||
_plainWindow?.Open();
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
_adminWindow?.Close();
|
||||
_plainWindow?.Close();
|
||||
}
|
||||
|
||||
private void SelectChannel(NetUserId uid)
|
||||
{
|
||||
_adminWindow ??= new BwoinkWindow(this);
|
||||
_adminWindow.SelectChannel(uid);
|
||||
OnBwoinkTextMessageRecieved?.Invoke(this, message);
|
||||
}
|
||||
|
||||
public void Send(NetUserId channelId, string text)
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
using Content.Client.Administration.UI.Tabs;
|
||||
using Content.Client.HUD;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
|
||||
namespace Content.Client.Administration.UI
|
||||
{
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class AdminMenuWindow : DefaultWindow
|
||||
{
|
||||
[Dependency] private readonly IGameHud? _gameHud = default!;
|
||||
|
||||
public AdminMenuWindow()
|
||||
{
|
||||
MinSize = (500, 250);
|
||||
@@ -27,19 +21,5 @@ namespace Content.Client.Administration.UI
|
||||
MasterTabContainer.SetTabTitle(5, Loc.GetString("admin-menu-players-tab"));
|
||||
MasterTabContainer.SetTabTitle(6, Loc.GetString("admin-menu-objects-tab"));
|
||||
}
|
||||
|
||||
protected override void EnteredTree()
|
||||
{
|
||||
base.EnteredTree();
|
||||
if (_gameHud != null)
|
||||
_gameHud.AdminButtonDown = true;
|
||||
}
|
||||
|
||||
protected override void ExitedTree()
|
||||
{
|
||||
base.ExitedTree();
|
||||
if (_gameHud != null)
|
||||
_gameHud.AdminButtonDown = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ using Content.Client.Administration.Systems;
|
||||
using Content.Client.Administration.UI.CustomControls;
|
||||
using Content.Client.Administration.UI.Tabs.AdminTab;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Client.UserInterface.Systems.Bwoink;
|
||||
using Content.Shared.Administration;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Console;
|
||||
@@ -27,15 +28,16 @@ namespace Content.Client.Administration.UI
|
||||
{
|
||||
[Dependency] private readonly IClientAdminManager _adminManager = default!;
|
||||
[Dependency] private readonly IClientConsoleHost _console = default!;
|
||||
private readonly AdminAHelpUIHandler _adminAHelpHelper;
|
||||
|
||||
private readonly BwoinkSystem _bwoinkSystem;
|
||||
//private readonly BwoinkSystem _bwoinkSystem;
|
||||
private PlayerInfo? _currentPlayer = default;
|
||||
|
||||
public BwoinkWindow(BwoinkSystem bs)
|
||||
public BwoinkWindow(AdminAHelpUIHandler adminAHelpHelper)
|
||||
{
|
||||
_adminAHelpHelper = adminAHelpHelper;
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
_bwoinkSystem = bs;
|
||||
|
||||
_adminManager.AdminStatusUpdated += FixButtons;
|
||||
FixButtons();
|
||||
@@ -57,7 +59,7 @@ namespace Content.Client.Administration.UI
|
||||
var sb = new StringBuilder();
|
||||
sb.Append(info.Connected ? '●' : '○');
|
||||
sb.Append(' ');
|
||||
if (_bwoinkSystem.TryGetChannel(info.SessionId, out var panel) && panel.Unread > 0)
|
||||
if (_adminAHelpHelper.TryGetChannel(info.SessionId, out var panel) && panel.Unread > 0)
|
||||
{
|
||||
if (panel.Unread < 11)
|
||||
sb.Append(new Rune('➀' + (panel.Unread-1)));
|
||||
@@ -76,8 +78,8 @@ namespace Content.Client.Administration.UI
|
||||
|
||||
ChannelSelector.Comparison = (a, b) =>
|
||||
{
|
||||
var aChannelExists = _bwoinkSystem.TryGetChannel(a.SessionId, out var ach);
|
||||
var bChannelExists = _bwoinkSystem.TryGetChannel(b.SessionId, out var bch);
|
||||
var aChannelExists = _adminAHelpHelper.TryGetChannel(a.SessionId, out var ach);
|
||||
var bChannelExists = _adminAHelpHelper.TryGetChannel(b.SessionId, out var bch);
|
||||
if (!aChannelExists && !bChannelExists)
|
||||
return 0;
|
||||
|
||||
@@ -174,7 +176,7 @@ namespace Content.Client.Administration.UI
|
||||
var sb = new StringBuilder();
|
||||
sb.Append(pl.Connected ? '●' : '○');
|
||||
sb.Append(' ');
|
||||
if (_bwoinkSystem.TryGetChannel(pl.SessionId, out var panel) && panel.Unread > 0)
|
||||
if (_adminAHelpHelper.TryGetChannel(pl.SessionId, out var panel) && panel.Unread > 0)
|
||||
{
|
||||
if (panel.Unread < 11)
|
||||
sb.Append(new Rune('➀' + (panel.Unread-1)));
|
||||
@@ -200,7 +202,7 @@ namespace Content.Client.Administration.UI
|
||||
{
|
||||
foreach (var bw in BwoinkArea.Children)
|
||||
bw.Visible = false;
|
||||
var panel = _bwoinkSystem.EnsurePanel(ch);
|
||||
var panel = _adminAHelpHelper.EnsurePanel(ch);
|
||||
panel.Visible = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#nullable enable
|
||||
using System;
|
||||
using Content.Client.Administration.Systems;
|
||||
using Content.Client.UserInterface.Systems.Bwoink;
|
||||
using Content.Shared.Administration;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
@@ -13,17 +14,15 @@ namespace Content.Client.Administration.UI.CustomControls
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class BwoinkPanel : BoxContainer
|
||||
{
|
||||
private readonly BwoinkSystem _bwoinkSystem;
|
||||
public readonly NetUserId ChannelId;
|
||||
private readonly Action<string> _messageSender;
|
||||
|
||||
public int Unread { get; private set; } = 0;
|
||||
public DateTime LastMessage { get; private set; } = DateTime.MinValue;
|
||||
|
||||
public BwoinkPanel(BwoinkSystem bwoinkSys, NetUserId userId)
|
||||
public BwoinkPanel(Action<string> messageSender)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
_bwoinkSystem = bwoinkSys;
|
||||
ChannelId = userId;
|
||||
_messageSender = messageSender;
|
||||
|
||||
OnVisibilityChanged += c =>
|
||||
{
|
||||
@@ -38,7 +37,7 @@ namespace Content.Client.Administration.UI.CustomControls
|
||||
if (string.IsNullOrWhiteSpace(args.Text))
|
||||
return;
|
||||
|
||||
_bwoinkSystem.Send(ChannelId, args.Text);
|
||||
_messageSender.Invoke(args.Text);
|
||||
SenderLineEdit.Clear();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
<Control xmlns="https://spacestation14.io"
|
||||
MinSize="64 64">
|
||||
<PanelContainer HorizontalAlignment="Right"
|
||||
VerticalAlignment="Top">
|
||||
<GridContainer Name="AlertContainer" Access="Public" MaxGridHeight="64" ExpandBackwards="True">
|
||||
|
||||
</GridContainer>
|
||||
</PanelContainer>
|
||||
</Control>
|
||||
@@ -1,344 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Content.Client.Chat.Managers;
|
||||
using Content.Client.Chat.UI;
|
||||
using Content.Shared.Alert;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
|
||||
namespace Content.Client.Alerts.UI;
|
||||
|
||||
public sealed class AlertsFramePresenter : IDisposable
|
||||
{
|
||||
[Dependency] private readonly IEntitySystemManager _systemManager = default!;
|
||||
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
|
||||
[Dependency] private readonly IChatManager _chatManager = default!;
|
||||
|
||||
private IAlertsFrameView _alertsFrame;
|
||||
private ClientAlertsSystem? _alertsSystem;
|
||||
|
||||
public AlertsFramePresenter()
|
||||
{
|
||||
// This is a lot easier than a factory
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
_alertsFrame = new AlertsUI(_chatManager);
|
||||
_userInterfaceManager.StateRoot.AddChild((AlertsUI) _alertsFrame);
|
||||
|
||||
// This is required so that if we load after the system is initialized, we can bind to it immediately
|
||||
if (_systemManager.TryGetEntitySystem<ClientAlertsSystem>(out var alertsSystem))
|
||||
SystemBindingChanged(alertsSystem);
|
||||
|
||||
_systemManager.SystemLoaded += OnSystemLoaded;
|
||||
_systemManager.SystemUnloaded += OnSystemUnloaded;
|
||||
|
||||
_alertsFrame.AlertPressed += OnAlertPressed;
|
||||
|
||||
// initially populate the frame if system is available
|
||||
var alerts = alertsSystem?.ActiveAlerts;
|
||||
if (alerts != null)
|
||||
{
|
||||
SystemOnSyncAlerts(alertsSystem, alerts);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
_userInterfaceManager.StateRoot.RemoveChild((AlertsUI) _alertsFrame);
|
||||
_alertsFrame.Dispose();
|
||||
_alertsFrame = null!;
|
||||
|
||||
SystemBindingChanged(null);
|
||||
_systemManager.SystemLoaded -= OnSystemLoaded;
|
||||
_systemManager.SystemUnloaded -= OnSystemUnloaded;
|
||||
}
|
||||
|
||||
private void OnAlertPressed(object? sender, AlertType e)
|
||||
{
|
||||
_alertsSystem?.AlertClicked(e);
|
||||
}
|
||||
|
||||
private void SystemOnClearAlerts(object? sender, EventArgs e)
|
||||
{
|
||||
_alertsFrame.ClearAllControls();
|
||||
}
|
||||
|
||||
private void SystemOnSyncAlerts(object? sender, IReadOnlyDictionary<AlertKey, AlertState> e)
|
||||
{
|
||||
if (sender is ClientAlertsSystem system)
|
||||
_alertsFrame.SyncControls(system, system.AlertOrder, e);
|
||||
}
|
||||
|
||||
//TODO: This system binding boilerplate seems to be duplicated between every presenter
|
||||
// prob want to pull it out into a generic object with callbacks for Onbind/OnUnbind
|
||||
#region System Binding
|
||||
|
||||
private void OnSystemLoaded(object? sender, SystemChangedArgs args)
|
||||
{
|
||||
if (args.System is ClientAlertsSystem system) SystemBindingChanged(system);
|
||||
}
|
||||
|
||||
private void OnSystemUnloaded(object? sender, SystemChangedArgs args)
|
||||
{
|
||||
if (args.System is ClientAlertsSystem) SystemBindingChanged(null);
|
||||
}
|
||||
|
||||
private void SystemBindingChanged(ClientAlertsSystem? newSystem)
|
||||
{
|
||||
if (newSystem is null)
|
||||
{
|
||||
if (_alertsSystem is null)
|
||||
return;
|
||||
|
||||
UnbindFromSystem();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_alertsSystem is null)
|
||||
{
|
||||
BindToSystem(newSystem);
|
||||
return;
|
||||
}
|
||||
|
||||
UnbindFromSystem();
|
||||
BindToSystem(newSystem);
|
||||
}
|
||||
}
|
||||
|
||||
private void BindToSystem(ClientAlertsSystem system)
|
||||
{
|
||||
_alertsSystem = system;
|
||||
system.SyncAlerts += SystemOnSyncAlerts;
|
||||
system.ClearAlerts += SystemOnClearAlerts;
|
||||
}
|
||||
|
||||
private void UnbindFromSystem()
|
||||
{
|
||||
var system = _alertsSystem;
|
||||
|
||||
if (system is null)
|
||||
throw new InvalidOperationException();
|
||||
|
||||
system.SyncAlerts -= SystemOnSyncAlerts;
|
||||
system.ClearAlerts -= SystemOnClearAlerts;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is the frame of vertical set of alerts that show up on the HUD.
|
||||
/// </summary>
|
||||
public interface IAlertsFrameView : IDisposable
|
||||
{
|
||||
event EventHandler<AlertType>? AlertPressed;
|
||||
|
||||
void SyncControls(AlertsSystem alertsSystem, AlertOrderPrototype? alertOrderPrototype,
|
||||
IReadOnlyDictionary<AlertKey, AlertState> alertStates);
|
||||
void ClearAllControls();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The status effects display on the right side of the screen.
|
||||
/// </summary>
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class AlertsUI : Control, IAlertsFrameView
|
||||
{
|
||||
// also known as Control.Children?
|
||||
private readonly Dictionary<AlertKey, AlertControl> _alertControls = new();
|
||||
|
||||
public AlertsUI(IChatManager chatManager)
|
||||
{
|
||||
_chatManager = chatManager;
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public void SyncControls(AlertsSystem alertsSystem, AlertOrderPrototype? alertOrderPrototype,
|
||||
IReadOnlyDictionary<AlertKey, AlertState> alertStates)
|
||||
{
|
||||
// remove any controls with keys no longer present
|
||||
if (SyncRemoveControls(alertStates)) return;
|
||||
|
||||
// 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
|
||||
SyncUpdateControls(alertsSystem, alertOrderPrototype, alertStates);
|
||||
}
|
||||
|
||||
public void ClearAllControls()
|
||||
{
|
||||
foreach (var alertControl in _alertControls.Values)
|
||||
{
|
||||
alertControl.OnPressed -= AlertControlPressed;
|
||||
alertControl.Dispose();
|
||||
}
|
||||
|
||||
_alertControls.Clear();
|
||||
}
|
||||
|
||||
public event EventHandler<AlertType>? AlertPressed;
|
||||
|
||||
//TODO: This control caring about it's layout relative to other controls in the tree is terrible
|
||||
// the presenters or gamescreen should be dealing with this
|
||||
// probably want to tackle this after chatbox gets MVP'd
|
||||
#region Spaghetti
|
||||
|
||||
public const float ChatSeparation = 38f;
|
||||
private readonly IChatManager _chatManager;
|
||||
|
||||
protected override void EnteredTree()
|
||||
{
|
||||
base.EnteredTree();
|
||||
_chatManager.OnChatBoxResized += OnChatResized;
|
||||
OnChatResized(new ChatResizedEventArgs(HudChatBox.InitialChatBottom));
|
||||
}
|
||||
|
||||
protected override void ExitedTree()
|
||||
{
|
||||
base.ExitedTree();
|
||||
_chatManager.OnChatBoxResized -= OnChatResized;
|
||||
}
|
||||
|
||||
private void OnChatResized(ChatResizedEventArgs chatResizedEventArgs)
|
||||
{
|
||||
// resize us to fit just below the chat box
|
||||
if (_chatManager.CurrentChatBox != null)
|
||||
LayoutContainer.SetMarginTop(this, chatResizedEventArgs.NewBottom + ChatSeparation);
|
||||
else
|
||||
LayoutContainer.SetMarginTop(this, 250);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
// This makes no sense but I'm leaving it in place in case I break anything by removing it.
|
||||
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
|
||||
base.Resized();
|
||||
AlertContainer.MaxGridHeight = Height;
|
||||
}
|
||||
|
||||
protected override void UIScaleChanged()
|
||||
{
|
||||
AlertContainer.MaxGridHeight = Height;
|
||||
base.UIScaleChanged();
|
||||
}
|
||||
|
||||
private bool SyncRemoveControls(IReadOnlyDictionary<AlertKey, AlertState> alertStates)
|
||||
{
|
||||
var toRemove = new List<AlertKey>();
|
||||
foreach (var existingKey in _alertControls.Keys)
|
||||
{
|
||||
if (!alertStates.ContainsKey(existingKey)) toRemove.Add(existingKey);
|
||||
}
|
||||
|
||||
foreach (var alertKeyToRemove in toRemove)
|
||||
{
|
||||
_alertControls.Remove(alertKeyToRemove, out var control);
|
||||
if (control == null) return true;
|
||||
AlertContainer.Children.Remove(control);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void SyncUpdateControls(AlertsSystem alertsSystem, AlertOrderPrototype? alertOrderPrototype,
|
||||
IReadOnlyDictionary<AlertKey, AlertState> alertStates)
|
||||
{
|
||||
foreach (var (alertKey, alertState) in alertStates)
|
||||
{
|
||||
if (!alertKey.AlertType.HasValue)
|
||||
{
|
||||
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 (!alertsSystem.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)
|
||||
{
|
||||
// key is the same, simply update the existing control severity / cooldown
|
||||
existingAlertControl.SetSeverity(alertState.Severity);
|
||||
existingAlertControl.Cooldown = alertState.Cooldown;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (existingAlertControl != null) AlertContainer.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, alertState);
|
||||
|
||||
//TODO: Can the presenter sort the states before giving it to us?
|
||||
if (alertOrderPrototype != null)
|
||||
{
|
||||
var added = false;
|
||||
foreach (var alertControl in AlertContainer.Children)
|
||||
{
|
||||
if (alertOrderPrototype.Compare(newAlert, ((AlertControl) alertControl).Alert) >= 0)
|
||||
continue;
|
||||
|
||||
var idx = alertControl.GetPositionInParent();
|
||||
AlertContainer.Children.Add(newAlertControl);
|
||||
newAlertControl.SetPositionInParent(idx);
|
||||
added = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!added) AlertContainer.Children.Add(newAlertControl);
|
||||
}
|
||||
else
|
||||
AlertContainer.Children.Add(newAlertControl);
|
||||
|
||||
_alertControls[newAlert.AlertKey] = newAlertControl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private AlertControl CreateAlertControl(AlertPrototype alert, AlertState alertState)
|
||||
{
|
||||
var alertControl = new AlertControl(alert, alertState.Severity)
|
||||
{
|
||||
Cooldown = alertState.Cooldown
|
||||
};
|
||||
alertControl.OnPressed += AlertControlPressed;
|
||||
return alertControl;
|
||||
}
|
||||
|
||||
private void AlertControlPressed(BaseButton.ButtonEventArgs args)
|
||||
{
|
||||
if (args.Button is not AlertControl control)
|
||||
return;
|
||||
|
||||
if (args.Event.Function != EngineKeyFunctions.UIClick)
|
||||
return;
|
||||
|
||||
AlertPressed?.Invoke(this, control.Alert.AlertType);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Client.UserInterface.Systems.EscapeMenu;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
|
||||
namespace Content.Client.Changelog
|
||||
@@ -13,8 +11,6 @@ namespace Content.Client.Changelog
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
OnPressed += OnChangelogPressed;
|
||||
|
||||
// So that measuring before opening returns a correct height,
|
||||
// and the window has the correct size when opened.
|
||||
Text = " ";
|
||||
@@ -35,11 +31,6 @@ namespace Content.Client.Changelog
|
||||
_changelogManager.NewChangelogEntriesChanged -= UpdateStuff;
|
||||
}
|
||||
|
||||
private void OnChangelogPressed(ButtonEventArgs obj)
|
||||
{
|
||||
IoCManager.Resolve<IUserInterfaceManager>().GetUIController<ChangelogUIController>().ToggleWindow();
|
||||
}
|
||||
|
||||
private void UpdateStuff()
|
||||
{
|
||||
if (_changelogManager.NewChangelogEntries)
|
||||
|
||||
@@ -205,7 +205,7 @@ namespace Content.Client.Changelog
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
IoCManager.Resolve<IUserInterfaceManager>().GetUIController<ChangelogUIController>().ToggleWindow();
|
||||
IoCManager.Resolve<IUserInterfaceManager>().GetUIController<ChangelogUIController>().OpenWindow();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
58
Content.Client/CharacterInfo/CharacterInfoSystem.cs
Normal file
58
Content.Client/CharacterInfo/CharacterInfoSystem.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using Content.Shared.CharacterInfo;
|
||||
using Content.Shared.Objectives;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Player;
|
||||
|
||||
namespace Content.Client.CharacterInfo;
|
||||
|
||||
public sealed class CharacterInfoSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IPlayerManager _players = default!;
|
||||
|
||||
public event Action<CharacterData>? OnCharacterUpdate;
|
||||
public event Action? OnCharacterDetached;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<PlayerAttachSysMessage>(OnPlayerAttached);
|
||||
|
||||
SubscribeNetworkEvent<CharacterInfoEvent>(OnCharacterInfoEvent);
|
||||
}
|
||||
|
||||
public void RequestCharacterInfo()
|
||||
{
|
||||
var entity = _players.LocalPlayer?.ControlledEntity;
|
||||
if (entity == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RaiseNetworkEvent(new RequestCharacterInfoEvent(entity.Value));
|
||||
}
|
||||
|
||||
private void OnPlayerAttached(PlayerAttachSysMessage msg)
|
||||
{
|
||||
if (msg.AttachedEntity == default)
|
||||
{
|
||||
OnCharacterDetached?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCharacterInfoEvent(CharacterInfoEvent msg, EntitySessionEventArgs args)
|
||||
{
|
||||
var sprite = CompOrNull<ISpriteComponent>(msg.EntityUid);
|
||||
var data = new CharacterData(msg.JobTitle, msg.Objectives, msg.Briefing, sprite, Name(msg.EntityUid));
|
||||
|
||||
OnCharacterUpdate?.Invoke(data);
|
||||
}
|
||||
|
||||
public readonly record struct CharacterData(
|
||||
string Job,
|
||||
Dictionary<string, List<ConditionInfo>> Objectives,
|
||||
string Briefing,
|
||||
ISpriteComponent? Sprite,
|
||||
string EntityName
|
||||
);
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
using Content.Client.CharacterInterface;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
|
||||
namespace Content.Client.CharacterInfo.Components
|
||||
{
|
||||
[RegisterComponent]
|
||||
public sealed class CharacterInfoComponent : Component, ICharacterUI
|
||||
{
|
||||
public CharacterInfoControl Control = default!;
|
||||
|
||||
public Control Scene { get; set; } = default!;
|
||||
public UIPriority Priority => UIPriority.Info;
|
||||
|
||||
public void Opened()
|
||||
{
|
||||
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<CharacterInfoSystem>().RequestCharacterInfo(Owner);
|
||||
}
|
||||
|
||||
public sealed class CharacterInfoControl : BoxContainer
|
||||
{
|
||||
public SpriteView SpriteView { get; }
|
||||
public Label NameLabel { get; }
|
||||
public Label SubText { get; }
|
||||
|
||||
public BoxContainer ObjectivesContainer { get; }
|
||||
|
||||
public CharacterInfoControl()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
Orientation = LayoutOrientation.Vertical;
|
||||
|
||||
AddChild(new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Horizontal,
|
||||
Children =
|
||||
{
|
||||
(SpriteView = new SpriteView { OverrideDirection = Direction.South, Scale = (2,2)}),
|
||||
new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Vertical,
|
||||
VerticalAlignment = VAlignment.Top,
|
||||
Children =
|
||||
{
|
||||
(NameLabel = new Label()),
|
||||
(SubText = new Label
|
||||
{
|
||||
VerticalAlignment = VAlignment.Top,
|
||||
StyleClasses = {StyleBase.StyleClassLabelSubText},
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
AddChild(new Label
|
||||
{
|
||||
Text = Loc.GetString("character-info-objectives-label"),
|
||||
HorizontalAlignment = HAlignment.Center
|
||||
});
|
||||
ObjectivesContainer = new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Vertical
|
||||
};
|
||||
AddChild(ObjectivesContainer);
|
||||
|
||||
AddChild(new Placeholder
|
||||
{
|
||||
PlaceholderText = Loc.GetString("character-info-roles-antagonist-text")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.CharacterInfo;
|
||||
using Content.Shared.Objectives;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.Utility;
|
||||
|
||||
namespace Content.Client.CharacterInfo.Components;
|
||||
|
||||
public sealed class CharacterInfoSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SpriteSystem _sprite = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeNetworkEvent<CharacterInfoEvent>(OnCharacterInfoEvent);
|
||||
SubscribeLocalEvent<CharacterInfoComponent, ComponentAdd>(OnComponentAdd);
|
||||
}
|
||||
|
||||
private void OnComponentAdd(EntityUid uid, CharacterInfoComponent component, ComponentAdd args)
|
||||
{
|
||||
component.Scene = component.Control = new CharacterInfoComponent.CharacterInfoControl();
|
||||
}
|
||||
|
||||
public void RequestCharacterInfo(EntityUid entityUid)
|
||||
{
|
||||
RaiseNetworkEvent(new RequestCharacterInfoEvent(entityUid));
|
||||
}
|
||||
|
||||
private void OnCharacterInfoEvent(CharacterInfoEvent msg, EntitySessionEventArgs args)
|
||||
{
|
||||
if (!EntityManager.TryGetComponent(msg.EntityUid, out CharacterInfoComponent? characterInfoComponent))
|
||||
return;
|
||||
|
||||
UpdateUI(characterInfoComponent, msg.JobTitle, msg.Objectives, msg.Briefing);
|
||||
if (EntityManager.TryGetComponent(msg.EntityUid, out ISpriteComponent? spriteComponent))
|
||||
{
|
||||
characterInfoComponent.Control.SpriteView.Sprite = spriteComponent;
|
||||
}
|
||||
|
||||
if (!EntityManager.TryGetComponent(msg.EntityUid, out MetaDataComponent? metadata))
|
||||
return;
|
||||
characterInfoComponent.Control.NameLabel.Text = metadata.EntityName;
|
||||
}
|
||||
|
||||
private void UpdateUI(CharacterInfoComponent comp, string jobTitle, Dictionary<string, List<ConditionInfo>> objectives, string briefing)
|
||||
{
|
||||
comp.Control.SubText.Text = jobTitle;
|
||||
|
||||
comp.Control.ObjectivesContainer.RemoveAllChildren();
|
||||
foreach (var (groupId, objectiveConditions) in objectives)
|
||||
{
|
||||
var vbox = new BoxContainer
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Vertical,
|
||||
Modulate = Color.Gray
|
||||
};
|
||||
|
||||
vbox.AddChild(new Label
|
||||
{
|
||||
Text = groupId,
|
||||
Modulate = Color.LightSkyBlue
|
||||
});
|
||||
|
||||
foreach (var objectiveCondition in objectiveConditions)
|
||||
{
|
||||
var hbox = new BoxContainer
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Horizontal
|
||||
};
|
||||
hbox.AddChild(new ProgressTextureRect
|
||||
{
|
||||
Texture = _sprite.Frame0(objectiveCondition.SpriteSpecifier),
|
||||
Progress = objectiveCondition.Progress,
|
||||
VerticalAlignment = Control.VAlignment.Center
|
||||
});
|
||||
hbox.AddChild(new Control
|
||||
{
|
||||
MinSize = (10,0)
|
||||
});
|
||||
hbox.AddChild(new BoxContainer
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Vertical,
|
||||
Children =
|
||||
{
|
||||
new Label{Text = objectiveCondition.Title},
|
||||
new Label{Text = objectiveCondition.Description}
|
||||
}
|
||||
}
|
||||
);
|
||||
vbox.AddChild(hbox);
|
||||
}
|
||||
var briefinghBox = new BoxContainer
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Horizontal
|
||||
};
|
||||
|
||||
briefinghBox.AddChild(new Label
|
||||
{
|
||||
Text = briefing,
|
||||
Modulate = Color.Yellow
|
||||
});
|
||||
|
||||
vbox.AddChild(briefinghBox);
|
||||
comp.Control.ObjectivesContainer.AddChild(vbox);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
using Content.Client.CharacterInterface;
|
||||
using Robust.Client.UserInterface;
|
||||
|
||||
namespace Content.Client.CharacterInfo.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// An interface which is gathered to assemble the character window from multiple components
|
||||
/// </summary>
|
||||
public interface ICharacterUI
|
||||
{
|
||||
/// <summary>
|
||||
/// The control which holds the character user interface to be included in the window
|
||||
/// </summary>
|
||||
Control Scene { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The order it will appear in the character UI, higher is lower
|
||||
/// </summary>
|
||||
UIPriority Priority { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Called when the CharacterUi was opened
|
||||
/// </summary>
|
||||
void Opened(){}
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using Content.Client.CharacterInfo.Components;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared.GameObjects;
|
||||
using static Robust.Client.UserInterface.Controls.BoxContainer;
|
||||
|
||||
namespace Content.Client.CharacterInterface
|
||||
{
|
||||
/// <summary>
|
||||
/// A semi-abstract component which gets added to entities upon attachment and collects all character
|
||||
/// user interfaces into a single window and keybind for the user
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed class CharacterInterfaceComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Window to hold each of the character interfaces
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Null if it would otherwise be empty.
|
||||
/// </remarks>
|
||||
public CharacterWindow? Window { get; set; }
|
||||
|
||||
public List<ICharacterUI>? UIComponents;
|
||||
|
||||
/// <summary>
|
||||
/// A window that collects and shows all the individual character user interfaces
|
||||
/// </summary>
|
||||
public sealed class CharacterWindow : DefaultWindow
|
||||
{
|
||||
private readonly List<ICharacterUI> _windowComponents;
|
||||
|
||||
public CharacterWindow(List<ICharacterUI> windowComponents)
|
||||
{
|
||||
Title = Loc.GetString("character-info-title");
|
||||
|
||||
var contentsVBox = new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Vertical
|
||||
};
|
||||
|
||||
var mainScrollContainer = new ScrollContainer { };
|
||||
mainScrollContainer.AddChild(contentsVBox);
|
||||
|
||||
Contents.AddChild(mainScrollContainer);
|
||||
|
||||
windowComponents.Sort((a, b) => ((int) a.Priority).CompareTo((int) b.Priority));
|
||||
foreach (var element in windowComponents)
|
||||
{
|
||||
contentsVBox.AddChild(element.Scene);
|
||||
}
|
||||
|
||||
_windowComponents = windowComponents;
|
||||
}
|
||||
|
||||
protected override void Opened()
|
||||
{
|
||||
base.Opened();
|
||||
foreach (var windowComponent in _windowComponents)
|
||||
{
|
||||
windowComponent.Opened();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines ordering of the character user interface, small values come sooner
|
||||
/// </summary>
|
||||
public enum UIPriority
|
||||
{
|
||||
First = 0,
|
||||
Info = 5,
|
||||
Species = 100,
|
||||
Last = 99999
|
||||
}
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
using System.Linq;
|
||||
using Content.Client.CharacterInfo.Components;
|
||||
using Content.Client.HUD;
|
||||
using Content.Shared.Input;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Input.Binding;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
namespace Content.Client.CharacterInterface
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public sealed class CharacterInterfaceSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IGameHud _gameHud = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IInputManager _inputManager = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
CommandBinds.Builder
|
||||
.Bind(ContentKeyFunctions.OpenCharacterMenu,
|
||||
InputCmdHandler.FromDelegate(_ => HandleOpenCharacterMenu()))
|
||||
.Register<CharacterInterfaceSystem>();
|
||||
|
||||
SubscribeLocalEvent<CharacterInterfaceComponent, ComponentInit>(OnComponentInit);
|
||||
SubscribeLocalEvent<CharacterInterfaceComponent, ComponentRemove>(OnComponentRemove);
|
||||
SubscribeLocalEvent<CharacterInterfaceComponent, PlayerAttachedEvent>(OnPlayerAttached);
|
||||
SubscribeLocalEvent<CharacterInterfaceComponent, PlayerDetachedEvent>(OnPlayerDetached);
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
CommandBinds.Unregister<CharacterInterfaceSystem>();
|
||||
base.Shutdown();
|
||||
}
|
||||
|
||||
private void OnComponentInit(EntityUid uid, CharacterInterfaceComponent comp, ComponentInit args)
|
||||
{
|
||||
//Use all the character ui interfaced components to create the character window
|
||||
comp.UIComponents = EntityManager.GetComponents<ICharacterUI>(uid).ToList();
|
||||
if (comp.UIComponents.Count == 0)
|
||||
return;
|
||||
|
||||
comp.Window = new CharacterInterfaceComponent.CharacterWindow(comp.UIComponents)
|
||||
{
|
||||
SetSize = (545, 400)
|
||||
};
|
||||
|
||||
comp.Window.OnClose += () => _gameHud.CharacterButtonDown = false;
|
||||
}
|
||||
|
||||
private void OnComponentRemove(EntityUid uid, CharacterInterfaceComponent comp, ComponentRemove args)
|
||||
{
|
||||
if (comp.UIComponents != null)
|
||||
{
|
||||
foreach (var component in comp.UIComponents)
|
||||
{
|
||||
// Make sure these don't get deleted when the window is disposed.
|
||||
component.Scene.Orphan();
|
||||
}
|
||||
}
|
||||
|
||||
comp.UIComponents = null;
|
||||
|
||||
comp.Window?.Close();
|
||||
comp.Window = null;
|
||||
|
||||
_inputManager.SetInputCommand(ContentKeyFunctions.OpenCharacterMenu, null);
|
||||
}
|
||||
|
||||
private void OnPlayerAttached(EntityUid uid, CharacterInterfaceComponent comp, PlayerAttachedEvent args)
|
||||
{
|
||||
if (comp.Window == null)
|
||||
return;
|
||||
|
||||
_gameHud.CharacterButtonVisible = true;
|
||||
_gameHud.CharacterButtonToggled += ToggleWindow;
|
||||
}
|
||||
|
||||
private void ToggleWindow(bool toggle)
|
||||
{
|
||||
if (!TryComp(_playerManager.LocalPlayer?.Session?.AttachedEntity, out CharacterInterfaceComponent? comp))
|
||||
return;
|
||||
|
||||
if (toggle)
|
||||
comp.Window?.OpenCentered();
|
||||
else
|
||||
comp.Window?.Close();
|
||||
}
|
||||
|
||||
private void OnPlayerDetached(EntityUid uid, CharacterInterfaceComponent comp, PlayerDetachedEvent args)
|
||||
{
|
||||
if (comp.Window == null)
|
||||
return;
|
||||
|
||||
_gameHud.CharacterButtonVisible = false;
|
||||
_gameHud.CharacterButtonToggled -= ToggleWindow;
|
||||
comp.Window.Close();
|
||||
}
|
||||
|
||||
private void HandleOpenCharacterMenu()
|
||||
{
|
||||
if (_playerManager.LocalPlayer?.ControlledEntity == null
|
||||
|| !EntityManager.TryGetComponent(_playerManager.LocalPlayer.ControlledEntity, out CharacterInterfaceComponent? characterInterface))
|
||||
return;
|
||||
|
||||
var menu = characterInterface.Window;
|
||||
if (menu == null)
|
||||
return;
|
||||
|
||||
if (menu.IsOpen)
|
||||
{
|
||||
if (menu.IsAtFront())
|
||||
_setOpenValue(menu, false);
|
||||
else
|
||||
menu.MoveToFront();
|
||||
}
|
||||
else
|
||||
{
|
||||
_setOpenValue(menu, true);
|
||||
}
|
||||
}
|
||||
|
||||
private void _setOpenValue(DefaultWindow menu, bool value)
|
||||
{
|
||||
_gameHud.CharacterButtonDown = value;
|
||||
if (value)
|
||||
menu.OpenCentered();
|
||||
else
|
||||
menu.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
using Content.Shared.Chat;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Content.Client.Chat
|
||||
{
|
||||
public sealed class ChatHelper
|
||||
{
|
||||
public static Color ChatColor(ChatChannel channel) =>
|
||||
channel switch
|
||||
{
|
||||
ChatChannel.Server => Color.Orange,
|
||||
ChatChannel.Radio => Color.LimeGreen,
|
||||
ChatChannel.LOOC => Color.MediumTurquoise,
|
||||
ChatChannel.OOC => Color.LightSkyBlue,
|
||||
ChatChannel.Dead => Color.MediumPurple,
|
||||
ChatChannel.Admin => Color.Red,
|
||||
ChatChannel.Whisper => Color.DarkGray,
|
||||
_ => Color.LightGray
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
using Content.Client.Chat.UI;
|
||||
using Content.Client.Gameplay;
|
||||
using Content.Client.Viewport;
|
||||
using Content.Shared.Chat;
|
||||
using Content.Shared.Input;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Shared.Input.Binding;
|
||||
|
||||
namespace Content.Client.Chat
|
||||
{
|
||||
public static class ChatInput
|
||||
{
|
||||
public static void SetupChatInputHandlers(IInputManager inputManager, ChatBox chatBox)
|
||||
{
|
||||
inputManager.SetInputCommand(ContentKeyFunctions.FocusChat,
|
||||
InputCmdHandler.FromDelegate(_ => GameplayState.FocusChat(chatBox)));
|
||||
|
||||
inputManager.SetInputCommand(ContentKeyFunctions.FocusLocalChat,
|
||||
InputCmdHandler.FromDelegate(_ => GameplayState.FocusChannel(chatBox, ChatSelectChannel.Local)));
|
||||
|
||||
inputManager.SetInputCommand(ContentKeyFunctions.FocusWhisperChat,
|
||||
InputCmdHandler.FromDelegate(_ => GameplayState.FocusChannel(chatBox, ChatSelectChannel.Whisper)));
|
||||
|
||||
inputManager.SetInputCommand(ContentKeyFunctions.FocusOOC,
|
||||
InputCmdHandler.FromDelegate(_ => GameplayState.FocusChannel(chatBox, ChatSelectChannel.OOC)));
|
||||
|
||||
inputManager.SetInputCommand(ContentKeyFunctions.FocusAdminChat,
|
||||
InputCmdHandler.FromDelegate(_ => GameplayState.FocusChannel(chatBox, ChatSelectChannel.Admin)));
|
||||
|
||||
inputManager.SetInputCommand(ContentKeyFunctions.FocusRadio,
|
||||
InputCmdHandler.FromDelegate(_ => GameplayState.FocusChannel(chatBox, ChatSelectChannel.Radio)));
|
||||
|
||||
inputManager.SetInputCommand(ContentKeyFunctions.FocusDeadChat,
|
||||
InputCmdHandler.FromDelegate(_ => GameplayState.FocusChannel(chatBox, ChatSelectChannel.Dead)));
|
||||
|
||||
inputManager.SetInputCommand(ContentKeyFunctions.FocusConsoleChat,
|
||||
InputCmdHandler.FromDelegate(_ => GameplayState.FocusChannel(chatBox, ChatSelectChannel.Console)));
|
||||
|
||||
inputManager.SetInputCommand(ContentKeyFunctions.CycleChatChannelForward,
|
||||
InputCmdHandler.FromDelegate(_ => chatBox.CycleChatChannel(true)));
|
||||
|
||||
inputManager.SetInputCommand(ContentKeyFunctions.CycleChatChannelBackward,
|
||||
InputCmdHandler.FromDelegate(_ => chatBox.CycleChatChannel(false)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,65 +1,5 @@
|
||||
using Content.Client.Chat.Managers;
|
||||
using Content.Client.Chat.UI;
|
||||
using Content.Client.Examine;
|
||||
using Content.Shared.Chat;
|
||||
using Content.Shared.Examine;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Client.Chat;
|
||||
|
||||
public sealed class ChatSystem : SharedChatSystem
|
||||
{
|
||||
[Dependency] private readonly IChatManager _manager = default!;
|
||||
[Dependency] private readonly IPlayerManager _player = default!;
|
||||
[Dependency] private readonly ExamineSystem _examineSystem = default!;
|
||||
|
||||
public override void FrameUpdate(float frameTime)
|
||||
{
|
||||
base.FrameUpdate(frameTime);
|
||||
|
||||
var player = _player.LocalPlayer?.ControlledEntity;
|
||||
var predicate = static (EntityUid uid, (EntityUid compOwner, EntityUid? attachedEntity) data)
|
||||
=> uid == data.compOwner || uid == data.attachedEntity;
|
||||
var bubbles = _manager.GetSpeechBubbles();
|
||||
var playerPos = player != null ? Transform(player.Value).MapPosition : MapCoordinates.Nullspace;
|
||||
|
||||
var occluded = player != null && _examineSystem.IsOccluded(player.Value);
|
||||
|
||||
foreach (var (ent, bubs) in bubbles)
|
||||
{
|
||||
if (Deleted(ent))
|
||||
{
|
||||
SetBubbles(bubs, false);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ent == player)
|
||||
{
|
||||
SetBubbles(bubs, true);
|
||||
continue;
|
||||
}
|
||||
|
||||
var otherPos = Transform(ent).MapPosition;
|
||||
|
||||
if (occluded && !ExamineSystemShared.InRangeUnOccluded(
|
||||
playerPos,
|
||||
otherPos, 0f,
|
||||
(ent, player), predicate))
|
||||
{
|
||||
SetBubbles(bubs, false);
|
||||
continue;
|
||||
}
|
||||
|
||||
SetBubbles(bubs, true);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetBubbles(List<SpeechBubble> bubbles, bool value)
|
||||
{
|
||||
foreach (var bubble in bubbles)
|
||||
{
|
||||
bubble.Visible = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
public sealed class ChatSystem : SharedChatSystem {}
|
||||
|
||||
@@ -1,327 +1,29 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Client.Administration.Managers;
|
||||
using Content.Client.Chat.UI;
|
||||
using Content.Client.Gameplay;
|
||||
using Content.Client.Ghost;
|
||||
using Content.Client.Viewport;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Chat;
|
||||
using Robust.Client.Console;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.State;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Chat.Managers
|
||||
{
|
||||
internal sealed class ChatManager : IChatManager, IPostInjectInit
|
||||
internal sealed class ChatManager : IChatManager
|
||||
{
|
||||
private struct SpeechBubbleData
|
||||
{
|
||||
public string Message;
|
||||
public SpeechBubble.SpeechType Type;
|
||||
}
|
||||
[Dependency] private readonly IClientConsoleHost _consoleHost = default!;
|
||||
[Dependency] private readonly IClientAdminManager _adminMgr = default!;
|
||||
[Dependency] private readonly IEntitySystemManager _systems = default!;
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The max amount of chars allowed to fit in a single speech bubble.
|
||||
/// </summary>
|
||||
private const int SingleBubbleCharLimit = 100;
|
||||
|
||||
/// <summary>
|
||||
/// Base queue delay each speech bubble has.
|
||||
/// </summary>
|
||||
private const float BubbleDelayBase = 0.2f;
|
||||
|
||||
/// <summary>
|
||||
/// Factor multiplied by speech bubble char length to add to delay.
|
||||
/// </summary>
|
||||
private const float BubbleDelayFactor = 0.8f / SingleBubbleCharLimit;
|
||||
|
||||
/// <summary>
|
||||
/// The max amount of speech bubbles over a single entity at once.
|
||||
/// </summary>
|
||||
private const int SpeechBubbleCap = 4;
|
||||
|
||||
/// <summary>
|
||||
/// The max amount of characters an entity can send in one message
|
||||
/// </summary>
|
||||
public int MaxMessageLength => _cfg.GetCVar(CCVars.ChatMaxMessageLength);
|
||||
|
||||
private readonly List<StoredChatMessage> _history = new();
|
||||
public IReadOnlyList<StoredChatMessage> History => _history;
|
||||
|
||||
// currently enabled channel filters set by the user.
|
||||
// All values default to on, even if they aren't a filterable chat channel currently.
|
||||
// Note that these are persisted here, at the manager,
|
||||
// rather than the chatbox so that these settings persist between instances of different
|
||||
// chatboxes.
|
||||
public ChatChannel ChannelFilters { get; private set; } = (ChatChannel) ushort.MaxValue;
|
||||
|
||||
// Maintains which channels a client should be able to filter (for showing in the chatbox)
|
||||
// and select (for attempting to send on).
|
||||
// This may not always actually match with what the server will actually allow them to
|
||||
// send / receive on, it is only what the user can select in the UI. For example,
|
||||
// if a user is silenced from speaking for some reason this may still contain ChatChannel.Local, it is left up
|
||||
// to the server to handle invalid attempts to use particular channels and not send messages for
|
||||
// channels the user shouldn't be able to hear.
|
||||
//
|
||||
// Note that Command is an available selection in the chatbox channel selector,
|
||||
// which is not actually a chat channel but is always available.
|
||||
public ChatSelectChannel SelectableChannels { get; private set; }
|
||||
public ChatChannel FilterableChannels { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// For currently disabled chat filters,
|
||||
/// unread messages (messages received since the channel has been filtered out).
|
||||
/// </summary>
|
||||
private readonly Dictionary<ChatChannel, int> _unreadMessages = new();
|
||||
|
||||
public IReadOnlyDictionary<ChatChannel, int> UnreadMessages => _unreadMessages;
|
||||
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IClientNetManager _netManager = default!;
|
||||
[Dependency] private readonly IClientConsoleHost _consoleHost = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
|
||||
[Dependency] private readonly IClientAdminManager _adminMgr = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly IStateManager _stateManager = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Current chat box control. This can be modified, so do not depend on saving a reference to this.
|
||||
/// </summary>
|
||||
public ChatBox? CurrentChatBox { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when CurrentChatBox is resized (including after setting initial default size)
|
||||
/// </summary>
|
||||
public event Action<ChatResizedEventArgs>? OnChatBoxResized;
|
||||
|
||||
public event Action<ChatPermissionsUpdatedEventArgs>? ChatPermissionsUpdated;
|
||||
public event Action? UnreadMessageCountsUpdated;
|
||||
public event Action<StoredChatMessage>? MessageAdded;
|
||||
public event Action? FiltersUpdated;
|
||||
|
||||
private Control _speechBubbleRoot = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Speech bubbles that are currently visible on screen.
|
||||
/// We track them to push them up when new ones get added.
|
||||
/// </summary>
|
||||
private readonly Dictionary<EntityUid, List<SpeechBubble>> _activeSpeechBubbles =
|
||||
new();
|
||||
|
||||
/// <summary>
|
||||
/// Speech bubbles that are to-be-sent because of the "rate limit" they have.
|
||||
/// </summary>
|
||||
private readonly Dictionary<EntityUid, SpeechBubbleQueueData> _queuedSpeechBubbles
|
||||
= new();
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_sawmill = Logger.GetSawmill("chat");
|
||||
_sawmill.Level = LogLevel.Info;
|
||||
_netManager.RegisterNetMessage<MsgChatMessage>(OnChatMessage);
|
||||
|
||||
_speechBubbleRoot = new LayoutContainer();
|
||||
LayoutContainer.SetAnchorPreset(_speechBubbleRoot, LayoutContainer.LayoutPreset.Wide);
|
||||
_userInterfaceManager.StateRoot.AddChild(_speechBubbleRoot);
|
||||
_speechBubbleRoot.SetPositionFirst();
|
||||
_stateManager.OnStateChanged += _ => UpdateChannelPermissions();
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<EntityUid, List<SpeechBubble>> GetSpeechBubbles() => _activeSpeechBubbles;
|
||||
|
||||
public void PostInject()
|
||||
public void SendMessage(ReadOnlyMemory<char> text, ChatSelectChannel channel)
|
||||
{
|
||||
_adminMgr.AdminStatusUpdated += UpdateChannelPermissions;
|
||||
_playerManager.LocalPlayerChanged += OnLocalPlayerChanged;
|
||||
OnLocalPlayerChanged(new LocalPlayerChangedEventArgs(null, _playerManager.LocalPlayer));
|
||||
}
|
||||
|
||||
private void OnLocalPlayerChanged(LocalPlayerChangedEventArgs obj)
|
||||
{
|
||||
if (obj.OldPlayer != null)
|
||||
{
|
||||
obj.OldPlayer.EntityAttached -= OnLocalPlayerEntityAttached;
|
||||
obj.OldPlayer.EntityDetached -= OnLocalPlayerEntityDetached;
|
||||
}
|
||||
|
||||
if (obj.NewPlayer != null)
|
||||
{
|
||||
obj.NewPlayer.EntityAttached += OnLocalPlayerEntityAttached;
|
||||
obj.NewPlayer.EntityDetached += OnLocalPlayerEntityDetached;
|
||||
}
|
||||
|
||||
UpdateChannelPermissions();
|
||||
}
|
||||
|
||||
private void OnLocalPlayerEntityAttached(EntityAttachedEventArgs obj)
|
||||
{
|
||||
UpdateChannelPermissions();
|
||||
}
|
||||
|
||||
private void OnLocalPlayerEntityDetached(EntityDetachedEventArgs obj)
|
||||
{
|
||||
UpdateChannelPermissions();
|
||||
}
|
||||
|
||||
// go through all of the various channels and update filter / select permissions
|
||||
// appropriately, also enabling them if our enabledChannels dict doesn't have an entry
|
||||
// for any newly-granted channels
|
||||
private void UpdateChannelPermissions()
|
||||
{
|
||||
var oldSelectable = SelectableChannels;
|
||||
SelectableChannels = default;
|
||||
FilterableChannels = default;
|
||||
|
||||
// Can always send console stuff.
|
||||
SelectableChannels |= ChatSelectChannel.Console;
|
||||
|
||||
// can always send/recieve OOC
|
||||
SelectableChannels |= ChatSelectChannel.OOC;
|
||||
FilterableChannels |= ChatChannel.OOC;
|
||||
SelectableChannels |= ChatSelectChannel.LOOC;
|
||||
FilterableChannels |= ChatChannel.LOOC;
|
||||
|
||||
// can always hear server (nobody can actually send server messages).
|
||||
FilterableChannels |= ChatChannel.Server;
|
||||
|
||||
if (_stateManager.CurrentState is GameplayStateBase)
|
||||
{
|
||||
// can always hear local / radio / emote when in the game
|
||||
FilterableChannels |= ChatChannel.Local;
|
||||
FilterableChannels |= ChatChannel.Whisper;
|
||||
FilterableChannels |= ChatChannel.Radio;
|
||||
FilterableChannels |= ChatChannel.Emotes;
|
||||
|
||||
// Can only send local / radio / emote when attached to a non-ghost entity.
|
||||
// TODO: this logic is iffy (checking if controlling something that's NOT a ghost), is there a better way to check this?
|
||||
if (!IsGhost)
|
||||
{
|
||||
SelectableChannels |= ChatSelectChannel.Local;
|
||||
SelectableChannels |= ChatSelectChannel.Whisper;
|
||||
SelectableChannels |= ChatSelectChannel.Radio;
|
||||
SelectableChannels |= ChatSelectChannel.Emotes;
|
||||
}
|
||||
}
|
||||
|
||||
// Only ghosts and admins can send / see deadchat.
|
||||
if (_adminMgr.HasFlag(AdminFlags.Admin) || IsGhost)
|
||||
{
|
||||
FilterableChannels |= ChatChannel.Dead;
|
||||
SelectableChannels |= ChatSelectChannel.Dead;
|
||||
}
|
||||
|
||||
// only admins can see / filter asay
|
||||
if (_adminMgr.HasFlag(AdminFlags.Admin))
|
||||
{
|
||||
FilterableChannels |= ChatChannel.Admin;
|
||||
SelectableChannels |= ChatSelectChannel.Admin;
|
||||
}
|
||||
|
||||
// Necessary so that we always have a channel to fall back to.
|
||||
DebugTools.Assert((SelectableChannels & ChatSelectChannel.OOC) != 0, "OOC must always be available");
|
||||
DebugTools.Assert((FilterableChannels & ChatChannel.OOC) != 0, "OOC must always be available");
|
||||
|
||||
// let our chatbox know all the new settings
|
||||
ChatPermissionsUpdated?.Invoke(new ChatPermissionsUpdatedEventArgs {OldSelectableChannels = oldSelectable});
|
||||
}
|
||||
|
||||
public bool IsGhost => _playerManager.LocalPlayer?.ControlledEntity is {} uid &&
|
||||
uid.IsValid() &&
|
||||
_entityManager.HasComponent<GhostComponent>(uid);
|
||||
|
||||
public void FrameUpdate(FrameEventArgs delta)
|
||||
{
|
||||
// Update queued speech bubbles.
|
||||
if (_queuedSpeechBubbles.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var (entity, queueData) in _queuedSpeechBubbles.ShallowClone())
|
||||
{
|
||||
if (!_entityManager.EntityExists(entity))
|
||||
{
|
||||
_queuedSpeechBubbles.Remove(entity);
|
||||
continue;
|
||||
}
|
||||
|
||||
queueData.TimeLeft -= delta.DeltaSeconds;
|
||||
if (queueData.TimeLeft > 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (queueData.MessageQueue.Count == 0)
|
||||
{
|
||||
_queuedSpeechBubbles.Remove(entity);
|
||||
continue;
|
||||
}
|
||||
|
||||
var msg = queueData.MessageQueue.Dequeue();
|
||||
|
||||
queueData.TimeLeft += BubbleDelayBase + msg.Message.Length * BubbleDelayFactor;
|
||||
|
||||
// We keep the queue around while it has 0 items. This allows us to keep the timer.
|
||||
// When the timer hits 0 and there's no messages left, THEN we can clear it up.
|
||||
CreateSpeechBubble(entity, msg);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetChatBox(ChatBox chatBox)
|
||||
{
|
||||
CurrentChatBox = chatBox;
|
||||
}
|
||||
|
||||
public void ClearUnfilteredUnreads()
|
||||
{
|
||||
foreach (var channel in _unreadMessages.Keys.ToArray())
|
||||
{
|
||||
if ((ChannelFilters & channel) != 0)
|
||||
_unreadMessages.Remove(channel);
|
||||
}
|
||||
}
|
||||
|
||||
public void ChatBoxOnResized(ChatResizedEventArgs chatResizedEventArgs)
|
||||
{
|
||||
OnChatBoxResized?.Invoke(chatResizedEventArgs);
|
||||
}
|
||||
|
||||
public void RemoveSpeechBubble(EntityUid entityUid, SpeechBubble bubble)
|
||||
{
|
||||
bubble.Dispose();
|
||||
|
||||
var list = _activeSpeechBubbles[entityUid];
|
||||
list.Remove(bubble);
|
||||
|
||||
if (list.Count == 0)
|
||||
{
|
||||
_activeSpeechBubbles.Remove(entityUid);
|
||||
}
|
||||
}
|
||||
|
||||
public void OnChatBoxTextSubmitted(ChatBox chatBox, ReadOnlyMemory<char> text, ChatSelectChannel channel)
|
||||
{
|
||||
DebugTools.Assert(chatBox == CurrentChatBox);
|
||||
|
||||
var str = text.ToString();
|
||||
|
||||
switch (channel)
|
||||
{
|
||||
case ChatSelectChannel.Console:
|
||||
@@ -346,9 +48,10 @@ namespace Content.Client.Chat.Managers
|
||||
break;
|
||||
|
||||
case ChatSelectChannel.Dead:
|
||||
if (IsGhost)
|
||||
if (_systems.GetEntitySystemOrNull<GhostSystem>() is {IsGhost: true})
|
||||
goto case ChatSelectChannel.Local;
|
||||
else if (_adminMgr.HasFlag(AdminFlags.Admin))
|
||||
|
||||
if (_adminMgr.HasFlag(AdminFlags.Admin))
|
||||
_consoleHost.ExecuteCommand($"dsay \"{CommandParsing.Escape(str)}\"");
|
||||
else
|
||||
_sawmill.Warning("Tried to speak on deadchat without being ghost or admin.");
|
||||
@@ -370,190 +73,5 @@ namespace Content.Client.Chat.Managers
|
||||
throw new ArgumentOutOfRangeException(nameof(channel), channel, null);
|
||||
}
|
||||
}
|
||||
|
||||
public void OnFilterButtonToggled(ChatChannel channel, bool enabled)
|
||||
{
|
||||
if (enabled)
|
||||
{
|
||||
ChannelFilters |= channel;
|
||||
_unreadMessages.Remove(channel);
|
||||
UnreadMessageCountsUpdated?.Invoke();
|
||||
}
|
||||
else
|
||||
{
|
||||
ChannelFilters &= ~channel;
|
||||
}
|
||||
|
||||
FiltersUpdated?.Invoke();
|
||||
}
|
||||
|
||||
private void OnChatMessage(MsgChatMessage msg)
|
||||
{
|
||||
// Log all incoming chat to repopulate when filter is un-toggled
|
||||
if (!msg.HideChat)
|
||||
{
|
||||
var storedMessage = new StoredChatMessage(msg);
|
||||
_history.Add(storedMessage);
|
||||
MessageAdded?.Invoke(storedMessage);
|
||||
|
||||
if (!storedMessage.Read)
|
||||
{
|
||||
_sawmill.Debug($"Message filtered: {storedMessage.Channel}: {storedMessage.Message}");
|
||||
if (!_unreadMessages.TryGetValue(msg.Channel, out var count))
|
||||
count = 0;
|
||||
|
||||
count += 1;
|
||||
_unreadMessages[msg.Channel] = count;
|
||||
UnreadMessageCountsUpdated?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
// Local messages that have an entity attached get a speech bubble.
|
||||
if (msg.SenderEntity == default)
|
||||
return;
|
||||
|
||||
switch (msg.Channel)
|
||||
{
|
||||
case ChatChannel.Local:
|
||||
AddSpeechBubble(msg, SpeechBubble.SpeechType.Say);
|
||||
break;
|
||||
|
||||
case ChatChannel.Whisper:
|
||||
AddSpeechBubble(msg, SpeechBubble.SpeechType.Whisper);
|
||||
break;
|
||||
|
||||
case ChatChannel.Dead:
|
||||
if (!IsGhost)
|
||||
break;
|
||||
|
||||
AddSpeechBubble(msg, SpeechBubble.SpeechType.Say);
|
||||
break;
|
||||
|
||||
case ChatChannel.Emotes:
|
||||
AddSpeechBubble(msg, SpeechBubble.SpeechType.Emote);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void AddSpeechBubble(MsgChatMessage msg, SpeechBubble.SpeechType speechType)
|
||||
{
|
||||
if (!_entityManager.EntityExists(msg.SenderEntity))
|
||||
{
|
||||
_sawmill.Debug("Got local chat message with invalid sender entity: {0}", msg.SenderEntity);
|
||||
return;
|
||||
}
|
||||
|
||||
var messages = SplitMessage(FormattedMessage.RemoveMarkup(msg.Message));
|
||||
|
||||
foreach (var message in messages)
|
||||
{
|
||||
EnqueueSpeechBubble(msg.SenderEntity, message, speechType);
|
||||
}
|
||||
}
|
||||
|
||||
private List<string> SplitMessage(string msg)
|
||||
{
|
||||
// Split message into words separated by spaces.
|
||||
var words = msg.Split(' ');
|
||||
var messages = new List<string>();
|
||||
var currentBuffer = new List<string>();
|
||||
|
||||
// Really shoddy way to approximate word length.
|
||||
// Yes, I am aware of all the crimes here.
|
||||
// TODO: Improve this to use actual glyph width etc..
|
||||
var currentWordLength = 0;
|
||||
foreach (var word in words)
|
||||
{
|
||||
// +1 for the space.
|
||||
currentWordLength += word.Length + 1;
|
||||
|
||||
if (currentWordLength > SingleBubbleCharLimit)
|
||||
{
|
||||
// Too long for the current speech bubble, flush it.
|
||||
messages.Add(string.Join(" ", currentBuffer));
|
||||
currentBuffer.Clear();
|
||||
|
||||
currentWordLength = word.Length;
|
||||
|
||||
if (currentWordLength > SingleBubbleCharLimit)
|
||||
{
|
||||
// Word is STILL too long.
|
||||
// Truncate it with an ellipse.
|
||||
messages.Add($"{word.Substring(0, SingleBubbleCharLimit - 3)}...");
|
||||
currentWordLength = 0;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
currentBuffer.Add(word);
|
||||
}
|
||||
|
||||
if (currentBuffer.Count != 0)
|
||||
{
|
||||
// Don't forget the last bubble.
|
||||
messages.Add(string.Join(" ", currentBuffer));
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
private void EnqueueSpeechBubble(EntityUid entity, string contents, SpeechBubble.SpeechType speechType)
|
||||
{
|
||||
// Don't enqueue speech bubbles for other maps. TODO: Support multiple viewports/maps?
|
||||
if (_entityManager.GetComponent<TransformComponent>(entity).MapID != _eyeManager.CurrentMap)
|
||||
return;
|
||||
|
||||
if (!_queuedSpeechBubbles.TryGetValue(entity, out var queueData))
|
||||
{
|
||||
queueData = new SpeechBubbleQueueData();
|
||||
_queuedSpeechBubbles.Add(entity, queueData);
|
||||
}
|
||||
|
||||
queueData.MessageQueue.Enqueue(new SpeechBubbleData
|
||||
{
|
||||
Message = contents,
|
||||
Type = speechType,
|
||||
});
|
||||
}
|
||||
|
||||
private void CreateSpeechBubble(EntityUid entity, SpeechBubbleData speechData)
|
||||
{
|
||||
var bubble =
|
||||
SpeechBubble.CreateSpeechBubble(speechData.Type, speechData.Message, entity, _eyeManager, this, _entityManager);
|
||||
|
||||
if (_activeSpeechBubbles.TryGetValue(entity, out var existing))
|
||||
{
|
||||
// Push up existing bubbles above the mob's head.
|
||||
foreach (var existingBubble in existing)
|
||||
{
|
||||
existingBubble.VerticalOffset += bubble.ContentSize.Y;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
existing = new List<SpeechBubble>();
|
||||
_activeSpeechBubbles.Add(entity, existing);
|
||||
}
|
||||
|
||||
existing.Add(bubble);
|
||||
_speechBubbleRoot.AddChild(bubble);
|
||||
|
||||
if (existing.Count > SpeechBubbleCap)
|
||||
{
|
||||
// Get the oldest to start fading fast.
|
||||
var last = existing[0];
|
||||
last.FadeNow();
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class SpeechBubbleQueueData
|
||||
{
|
||||
/// <summary>
|
||||
/// Time left until the next speech bubble can appear.
|
||||
/// </summary>
|
||||
public float TimeLeft { get; set; }
|
||||
|
||||
public Queue<SpeechBubbleData> MessageQueue { get; } = new();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,55 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Content.Client.Chat.UI;
|
||||
using Content.Shared.Chat;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.Chat.Managers
|
||||
{
|
||||
public interface IChatManager
|
||||
{
|
||||
ChatChannel ChannelFilters { get; }
|
||||
ChatSelectChannel SelectableChannels { get; }
|
||||
ChatChannel FilterableChannels { get; }
|
||||
|
||||
void Initialize();
|
||||
|
||||
void FrameUpdate(FrameEventArgs delta);
|
||||
|
||||
void SetChatBox(ChatBox chatBox);
|
||||
|
||||
void RemoveSpeechBubble(EntityUid entityUid, SpeechBubble bubble);
|
||||
|
||||
/// <summary>
|
||||
/// Current chat box control. This can be modified, so do not depend on saving a reference to this.
|
||||
/// </summary>
|
||||
ChatBox? CurrentChatBox { get; }
|
||||
|
||||
IReadOnlyDictionary<EntityUid, List<SpeechBubble>> GetSpeechBubbles();
|
||||
IReadOnlyDictionary<ChatChannel, int> UnreadMessages { get; }
|
||||
IReadOnlyList<StoredChatMessage> History { get; }
|
||||
int MaxMessageLength { get; }
|
||||
bool IsGhost { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when CurrentChatBox is resized (including after setting initial default size)
|
||||
/// </summary>
|
||||
event Action<ChatResizedEventArgs>? OnChatBoxResized;
|
||||
|
||||
event Action<ChatPermissionsUpdatedEventArgs>? ChatPermissionsUpdated;
|
||||
event Action? UnreadMessageCountsUpdated;
|
||||
event Action<StoredChatMessage>? MessageAdded;
|
||||
event Action? FiltersUpdated;
|
||||
|
||||
void ClearUnfilteredUnreads();
|
||||
void ChatBoxOnResized(ChatResizedEventArgs chatResizedEventArgs);
|
||||
void OnChatBoxTextSubmitted(ChatBox chatBox, ReadOnlyMemory<char> text, ChatSelectChannel channel);
|
||||
void OnFilterButtonToggled(ChatChannel channel, bool enabled);
|
||||
}
|
||||
|
||||
public struct ChatPermissionsUpdatedEventArgs
|
||||
{
|
||||
public ChatSelectChannel OldSelectableChannels;
|
||||
public void SendMessage(ReadOnlyMemory<char> text, ChatSelectChannel channel);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
<Control xmlns="https://spacestation14.io"
|
||||
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
||||
xmlns:chatUI="clr-namespace:Content.Client.Chat.UI"
|
||||
MouseFilter="Stop"
|
||||
MinSize="200 128">
|
||||
<PanelContainer>
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BackgroundColor="#25252AAA" />
|
||||
</PanelContainer.PanelOverride>
|
||||
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<OutputPanel Name="Contents" VerticalExpand="True" />
|
||||
<PanelContainer StyleClasses="ChatSubPanel">
|
||||
<BoxContainer Orientation="Horizontal" SeparationOverride="4">
|
||||
<chatUI:ChannelSelectorButton Name="ChannelSelector" ToggleMode="True"
|
||||
StyleClasses="chatSelectorOptionButton" MinWidth="75" />
|
||||
<HistoryLineEdit Name="Input" PlaceHolder="{Loc 'hud-chatbox-info'}" HorizontalExpand="True"
|
||||
StyleClasses="chatLineEdit" />
|
||||
<chatUI:FilterButton Name="FilterButton" StyleClasses="chatFilterOptionButton" />
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</Control>
|
||||
@@ -1,754 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Client.Alerts.UI;
|
||||
using Content.Client.Chat.Managers;
|
||||
using Content.Client.Chat.TypingIndicator;
|
||||
using Content.Client.Resources;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Shared.Chat;
|
||||
using Content.Shared.Input;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Chat.UI
|
||||
{
|
||||
[GenerateTypedNameReferences]
|
||||
[Virtual]
|
||||
public partial class ChatBox : Control
|
||||
{
|
||||
[Dependency] protected readonly IChatManager ChatMgr = default!;
|
||||
|
||||
// order in which the available channel filters show up when available
|
||||
private static readonly ChatChannel[] ChannelFilterOrder =
|
||||
{
|
||||
ChatChannel.Local,
|
||||
ChatChannel.Whisper,
|
||||
ChatChannel.Emotes,
|
||||
ChatChannel.Radio,
|
||||
ChatChannel.OOC,
|
||||
ChatChannel.Dead,
|
||||
ChatChannel.Admin,
|
||||
ChatChannel.Server
|
||||
};
|
||||
|
||||
// order in which the channels show up in the channel selector
|
||||
private static readonly ChatSelectChannel[] ChannelSelectorOrder =
|
||||
{
|
||||
ChatSelectChannel.Local,
|
||||
ChatSelectChannel.Whisper,
|
||||
ChatSelectChannel.Emotes,
|
||||
ChatSelectChannel.Radio,
|
||||
ChatSelectChannel.LOOC,
|
||||
ChatSelectChannel.OOC,
|
||||
ChatSelectChannel.Dead,
|
||||
ChatSelectChannel.Admin
|
||||
// NOTE: Console is not in there and it can never be permanently selected.
|
||||
// You can, however, still submit commands as console by prefixing with /.
|
||||
};
|
||||
|
||||
public const char AliasLocal = '.';
|
||||
public const char AliasConsole = '/';
|
||||
public const char AliasDead = '\\';
|
||||
public const char AliasOOC = '[';
|
||||
public const char AliasEmotes = '@';
|
||||
public const char AliasAdmin = ']';
|
||||
public const char AliasRadio = ';';
|
||||
public const char AliasWhisper = ',';
|
||||
|
||||
private static readonly Dictionary<char, ChatSelectChannel> PrefixToChannel = new()
|
||||
{
|
||||
{AliasLocal, ChatSelectChannel.Local},
|
||||
{AliasWhisper, ChatSelectChannel.Whisper},
|
||||
{AliasConsole, ChatSelectChannel.Console},
|
||||
{AliasOOC, ChatSelectChannel.OOC},
|
||||
{AliasEmotes, ChatSelectChannel.Emotes},
|
||||
{AliasAdmin, ChatSelectChannel.Admin},
|
||||
{AliasRadio, ChatSelectChannel.Radio},
|
||||
{AliasDead, ChatSelectChannel.Dead}
|
||||
};
|
||||
|
||||
private static readonly Dictionary<ChatSelectChannel, char> ChannelPrefixes =
|
||||
PrefixToChannel.ToDictionary(kv => kv.Value, kv => kv.Key);
|
||||
|
||||
private const float FilterPopupWidth = 110;
|
||||
|
||||
/// <summary>
|
||||
/// The currently default channel that will be used if no prefix is specified.
|
||||
/// </summary>
|
||||
public ChatSelectChannel SelectedChannel { get; private set; } = ChatSelectChannel.OOC;
|
||||
|
||||
/// <summary>
|
||||
/// The "preferred" channel. Will be switched to if permissions change and the channel becomes available,
|
||||
/// such as by re-entering body. Gets changed if the user manually selects a channel with the buttons.
|
||||
/// </summary>
|
||||
public ChatSelectChannel PreferredChannel { get; set; } = ChatSelectChannel.OOC;
|
||||
|
||||
public bool ReleaseFocusOnEnter { get; set; } = true;
|
||||
|
||||
private readonly Popup _channelSelectorPopup;
|
||||
private readonly BoxContainer _channelSelectorHBox;
|
||||
private readonly Popup _filterPopup;
|
||||
private readonly PanelContainer _filterPopupPanel;
|
||||
private readonly BoxContainer _filterVBox;
|
||||
|
||||
/// <summary>
|
||||
/// When lobbyMode is false, will position / add to correct location in StateRoot and
|
||||
/// be resizable.
|
||||
/// wWen true, will leave layout up to parent and not be resizable.
|
||||
/// </summary>
|
||||
public ChatBox()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
LayoutContainer.SetMarginLeft(this, 4);
|
||||
LayoutContainer.SetMarginRight(this, 4);
|
||||
|
||||
_filterPopup = new Popup
|
||||
{
|
||||
Children =
|
||||
{
|
||||
(_filterPopupPanel = new PanelContainer
|
||||
{
|
||||
StyleClasses = {StyleNano.StyleClassBorderedWindowPanel},
|
||||
Children =
|
||||
{
|
||||
new BoxContainer
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Horizontal,
|
||||
Children =
|
||||
{
|
||||
new Control {MinSize = (4, 0)},
|
||||
(_filterVBox = new BoxContainer
|
||||
{
|
||||
Margin = new Thickness(0, 10),
|
||||
Orientation = BoxContainer.LayoutOrientation.Vertical,
|
||||
SeparationOverride = 4
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
_channelSelectorPopup = new Popup
|
||||
{
|
||||
Children =
|
||||
{
|
||||
(_channelSelectorHBox = new BoxContainer
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Horizontal,
|
||||
SeparationOverride = 1
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
ChannelSelector.OnToggled += OnChannelSelectorToggled;
|
||||
FilterButton.OnToggled += OnFilterButtonToggled;
|
||||
Input.OnKeyBindDown += InputKeyBindDown;
|
||||
Input.OnTextEntered += Input_OnTextEntered;
|
||||
Input.OnTextChanged += InputOnTextChanged;
|
||||
_channelSelectorPopup.OnPopupHide += OnChannelSelectorPopupHide;
|
||||
_filterPopup.OnPopupHide += OnFilterPopupHide;
|
||||
}
|
||||
|
||||
protected override void EnteredTree()
|
||||
{
|
||||
base.EnteredTree();
|
||||
|
||||
ChatMgr.MessageAdded += WriteChatMessage;
|
||||
ChatMgr.ChatPermissionsUpdated += OnChatPermissionsUpdated;
|
||||
ChatMgr.UnreadMessageCountsUpdated += UpdateUnreadMessageCounts;
|
||||
ChatMgr.FiltersUpdated += Repopulate;
|
||||
|
||||
// The chat manager may have messages logged from before there was a chat box.
|
||||
// In this case, these messages will be marked as unread despite the filters allowing them through.
|
||||
// Tell chat manager to clear these.
|
||||
ChatMgr.ClearUnfilteredUnreads();
|
||||
|
||||
ChatPermissionsUpdated(0);
|
||||
UpdateChannelSelectButton();
|
||||
Repopulate();
|
||||
}
|
||||
|
||||
protected override void ExitedTree()
|
||||
{
|
||||
base.ExitedTree();
|
||||
|
||||
ChatMgr.MessageAdded -= WriteChatMessage;
|
||||
ChatMgr.ChatPermissionsUpdated -= OnChatPermissionsUpdated;
|
||||
ChatMgr.UnreadMessageCountsUpdated -= UpdateUnreadMessageCounts;
|
||||
ChatMgr.FiltersUpdated -= Repopulate;
|
||||
}
|
||||
|
||||
private void OnChatPermissionsUpdated(ChatPermissionsUpdatedEventArgs eventArgs)
|
||||
{
|
||||
ChatPermissionsUpdated(eventArgs.OldSelectableChannels);
|
||||
}
|
||||
|
||||
private void ChatPermissionsUpdated(ChatSelectChannel oldSelectable)
|
||||
{
|
||||
// update the channel selector
|
||||
_channelSelectorHBox.Children.Clear();
|
||||
foreach (var selectableChannel in ChannelSelectorOrder)
|
||||
{
|
||||
if ((ChatMgr.SelectableChannels & selectableChannel) == 0)
|
||||
continue;
|
||||
|
||||
var newButton = new ChannelItemButton(selectableChannel);
|
||||
newButton.OnPressed += OnChannelSelectorItemPressed;
|
||||
_channelSelectorHBox.AddChild(newButton);
|
||||
}
|
||||
|
||||
// Selected channel no longer available, switch to OOC?
|
||||
if ((ChatMgr.SelectableChannels & SelectedChannel) == 0)
|
||||
{
|
||||
// Handle local -> dead mapping when you e.g. ghost.
|
||||
// Only necessary for admins because they always have deadchat
|
||||
// so the normal preferred check won't see it as newly available and do nothing.
|
||||
var mappedSelect = MapLocalIfGhost(SelectedChannel);
|
||||
if ((ChatMgr.SelectableChannels & mappedSelect) != 0)
|
||||
SafelySelectChannel(mappedSelect);
|
||||
else
|
||||
SafelySelectChannel(ChatSelectChannel.OOC);
|
||||
}
|
||||
|
||||
// If the preferred channel just became available, switch to it.
|
||||
var pref = MapLocalIfGhost(PreferredChannel);
|
||||
if ((oldSelectable & pref) == 0 && (ChatMgr.SelectableChannels & pref) != 0)
|
||||
SafelySelectChannel(pref);
|
||||
|
||||
// update the channel filters
|
||||
_filterVBox.Children.Clear();
|
||||
foreach (var channelFilter in ChannelFilterOrder)
|
||||
{
|
||||
if ((ChatMgr.FilterableChannels & channelFilter) == 0)
|
||||
continue;
|
||||
|
||||
int? unreadCount = null;
|
||||
if (ChatMgr.UnreadMessages.TryGetValue(channelFilter, out var unread))
|
||||
unreadCount = unread;
|
||||
|
||||
var newCheckBox = new ChannelFilterCheckbox(channelFilter, unreadCount)
|
||||
{
|
||||
Pressed = (ChatMgr.ChannelFilters & channelFilter) != 0
|
||||
};
|
||||
|
||||
newCheckBox.OnToggled += OnFilterCheckboxToggled;
|
||||
_filterVBox.AddChild(newCheckBox);
|
||||
}
|
||||
|
||||
UpdateChannelSelectButton();
|
||||
}
|
||||
|
||||
private void UpdateUnreadMessageCounts()
|
||||
{
|
||||
foreach (var channelFilter in _filterVBox.Children)
|
||||
{
|
||||
if (channelFilter is not ChannelFilterCheckbox filterCheckbox) continue;
|
||||
if (ChatMgr.UnreadMessages.TryGetValue(filterCheckbox.Channel, out var unread))
|
||||
{
|
||||
filterCheckbox.UpdateUnreadCount(unread);
|
||||
}
|
||||
else
|
||||
{
|
||||
filterCheckbox.UpdateUnreadCount(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnFilterCheckboxToggled(BaseButton.ButtonToggledEventArgs args)
|
||||
{
|
||||
if (args.Button is not ChannelFilterCheckbox checkbox)
|
||||
return;
|
||||
|
||||
ChatMgr.OnFilterButtonToggled(checkbox.Channel, checkbox.Pressed);
|
||||
}
|
||||
|
||||
private void OnFilterButtonToggled(BaseButton.ButtonToggledEventArgs args)
|
||||
{
|
||||
if (args.Pressed)
|
||||
{
|
||||
var globalPos = FilterButton.GlobalPosition;
|
||||
var (minX, minY) = _filterPopupPanel.MinSize;
|
||||
var box = UIBox2.FromDimensions(globalPos - (FilterPopupWidth, 0),
|
||||
(Math.Max(minX, FilterPopupWidth), minY));
|
||||
UserInterfaceManager.ModalRoot.AddChild(_filterPopup);
|
||||
_filterPopup.Open(box);
|
||||
}
|
||||
else
|
||||
{
|
||||
_filterPopup.Close();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnChannelSelectorToggled(BaseButton.ButtonToggledEventArgs args)
|
||||
{
|
||||
if (args.Pressed)
|
||||
{
|
||||
var globalLeft = GlobalPosition.X;
|
||||
var globalBot = GlobalPosition.Y + Height;
|
||||
var box = UIBox2.FromDimensions((globalLeft, globalBot), (SizeBox.Width, AlertsUI.ChatSeparation));
|
||||
UserInterfaceManager.ModalRoot.AddChild(_channelSelectorPopup);
|
||||
_channelSelectorPopup.Open(box);
|
||||
}
|
||||
else
|
||||
{
|
||||
_channelSelectorPopup.Close();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnFilterPopupHide()
|
||||
{
|
||||
OnPopupHide(_filterPopup, FilterButton);
|
||||
}
|
||||
|
||||
private void OnChannelSelectorPopupHide()
|
||||
{
|
||||
OnPopupHide(_channelSelectorPopup, ChannelSelector);
|
||||
}
|
||||
|
||||
private void OnPopupHide(Control popup, BaseButton button)
|
||||
{
|
||||
UserInterfaceManager.ModalRoot.RemoveChild(popup);
|
||||
|
||||
// this weird check here is because the hiding of the popup happens prior to the button
|
||||
// receiving the keydown, which would cause it to then become unpressed
|
||||
// and reopen immediately. To avoid this, if the popup was hidden due to clicking on the button,
|
||||
// we will not auto-unpress the button, instead leaving it up to the button toggle logic
|
||||
// (and this requires the button to be set to EnableAllKeybinds = true)
|
||||
if (UserInterfaceManager.CurrentlyHovered != button)
|
||||
{
|
||||
button.Pressed = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnChannelSelectorItemPressed(BaseButton.ButtonEventArgs obj)
|
||||
{
|
||||
if (obj.Button is not ChannelItemButton button)
|
||||
return;
|
||||
|
||||
PreferredChannel = button.Channel;
|
||||
SafelySelectChannel(button.Channel);
|
||||
_channelSelectorPopup.Close();
|
||||
}
|
||||
|
||||
public bool SafelySelectChannel(ChatSelectChannel toSelect)
|
||||
{
|
||||
toSelect = MapLocalIfGhost(toSelect);
|
||||
if ((ChatMgr.SelectableChannels & toSelect) == 0)
|
||||
return false;
|
||||
|
||||
SelectedChannel = toSelect;
|
||||
UpdateChannelSelectButton();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void UpdateChannelSelectButton()
|
||||
{
|
||||
var (prefixChannel, _) = SplitInputContents();
|
||||
|
||||
var channel = prefixChannel == 0 ? SelectedChannel : prefixChannel;
|
||||
|
||||
ChannelSelector.Text = ChannelSelectorName(channel);
|
||||
ChannelSelector.Modulate = ChannelSelectColor(channel);
|
||||
}
|
||||
|
||||
protected override void KeyBindDown(GUIBoundKeyEventArgs args)
|
||||
{
|
||||
base.KeyBindDown(args);
|
||||
|
||||
if (args.CanFocus)
|
||||
{
|
||||
Input.GrabKeyboardFocus();
|
||||
}
|
||||
}
|
||||
|
||||
public void CycleChatChannel(bool forward)
|
||||
{
|
||||
Input.IgnoreNext = true;
|
||||
|
||||
var idx = Array.IndexOf(ChannelSelectorOrder, SelectedChannel);
|
||||
do
|
||||
{
|
||||
// go over every channel until we find one we can actually select.
|
||||
idx += forward ? 1 : -1;
|
||||
idx = MathHelper.Mod(idx, ChannelSelectorOrder.Length);
|
||||
} while ((ChatMgr.SelectableChannels & ChannelSelectorOrder[idx]) == 0);
|
||||
|
||||
SafelySelectChannel(ChannelSelectorOrder[idx]);
|
||||
}
|
||||
|
||||
private void Repopulate()
|
||||
{
|
||||
Contents.Clear();
|
||||
|
||||
foreach (var msg in ChatMgr.History)
|
||||
{
|
||||
WriteChatMessage(msg);
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteChatMessage(StoredChatMessage message)
|
||||
{
|
||||
var messageText = FormattedMessage.EscapeText(message.Message);
|
||||
if (!string.IsNullOrEmpty(message.MessageWrap))
|
||||
{
|
||||
messageText = string.Format(message.MessageWrap, messageText);
|
||||
}
|
||||
|
||||
Logger.DebugS("chat", $"{message.Channel}: {messageText}");
|
||||
|
||||
if (IsFilteredOut(message.Channel))
|
||||
return;
|
||||
|
||||
// TODO: Can make this "smarter" later by only setting it false when the message has been scrolled to
|
||||
message.Read = true;
|
||||
|
||||
var color = message.MessageColorOverride != Color.Transparent
|
||||
? message.MessageColorOverride
|
||||
: ChatHelper.ChatColor(message.Channel);
|
||||
|
||||
AddLine(messageText, message.Channel, color);
|
||||
}
|
||||
|
||||
private bool IsFilteredOut(ChatChannel channel)
|
||||
{
|
||||
return (ChatMgr.ChannelFilters & channel) == 0;
|
||||
}
|
||||
|
||||
private void InputKeyBindDown(GUIBoundKeyEventArgs args)
|
||||
{
|
||||
if (args.Function == EngineKeyFunctions.TextReleaseFocus)
|
||||
{
|
||||
Input.ReleaseKeyboardFocus();
|
||||
args.Handle();
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Function == ContentKeyFunctions.CycleChatChannelForward)
|
||||
{
|
||||
CycleChatChannel(true);
|
||||
args.Handle();
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Function == ContentKeyFunctions.CycleChatChannelBackward)
|
||||
{
|
||||
CycleChatChannel(false);
|
||||
args.Handle();
|
||||
}
|
||||
}
|
||||
|
||||
private (ChatSelectChannel selChannel, ReadOnlyMemory<char> text) SplitInputContents()
|
||||
{
|
||||
var text = Input.Text.AsMemory().Trim();
|
||||
if (text.Length == 0)
|
||||
return default;
|
||||
|
||||
var prefixChar = text.Span[0];
|
||||
var channel = GetChannelFromPrefix(prefixChar);
|
||||
|
||||
if ((ChatMgr.SelectableChannels & channel) != 0)
|
||||
// Cut off prefix if it's valid and we can use the channel in question.
|
||||
text = text[1..];
|
||||
else
|
||||
channel = 0;
|
||||
|
||||
channel = MapLocalIfGhost(channel);
|
||||
|
||||
// Trim from start again to cut out any whitespace between the prefix and message, if any.
|
||||
return (channel, text.TrimStart());
|
||||
}
|
||||
|
||||
private void InputOnTextChanged(LineEdit.LineEditEventArgs obj)
|
||||
{
|
||||
// Update channel select button to correct channel if we have a prefix.
|
||||
UpdateChannelSelectButton();
|
||||
|
||||
// Warn typing indicator about change
|
||||
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<TypingIndicatorSystem>().ClientChangedChatText();
|
||||
}
|
||||
|
||||
private static ChatSelectChannel GetChannelFromPrefix(char prefix)
|
||||
{
|
||||
return PrefixToChannel.GetValueOrDefault(prefix);
|
||||
}
|
||||
|
||||
public static char GetPrefixFromChannel(ChatSelectChannel channel)
|
||||
{
|
||||
return ChannelPrefixes.GetValueOrDefault(channel);
|
||||
}
|
||||
|
||||
public static string ChannelSelectorName(ChatSelectChannel channel)
|
||||
{
|
||||
return Loc.GetString($"hud-chatbox-select-channel-{channel}");
|
||||
}
|
||||
|
||||
public static Color ChannelSelectColor(ChatSelectChannel channel)
|
||||
{
|
||||
return channel switch
|
||||
{
|
||||
ChatSelectChannel.Radio => Color.LimeGreen,
|
||||
ChatSelectChannel.LOOC => Color.MediumTurquoise,
|
||||
ChatSelectChannel.OOC => Color.LightSkyBlue,
|
||||
ChatSelectChannel.Dead => Color.MediumPurple,
|
||||
ChatSelectChannel.Admin => Color.Red,
|
||||
_ => Color.DarkGray
|
||||
};
|
||||
}
|
||||
|
||||
public void AddLine(string message, ChatChannel channel, Color color)
|
||||
{
|
||||
DebugTools.Assert(!Disposed);
|
||||
|
||||
var formatted = new FormattedMessage(3);
|
||||
formatted.PushColor(color);
|
||||
formatted.AddMarkup(message);
|
||||
formatted.Pop();
|
||||
Contents.AddMessage(formatted);
|
||||
}
|
||||
|
||||
private void Input_OnTextEntered(LineEdit.LineEditEventArgs args)
|
||||
{
|
||||
// Warn typing indicator about entered text
|
||||
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<TypingIndicatorSystem>().ClientSubmittedChatText();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(args.Text))
|
||||
{
|
||||
var (prefixChannel, text) = SplitInputContents();
|
||||
|
||||
// Check if message is longer than the character limit
|
||||
if (text.Length > ChatMgr.MaxMessageLength)
|
||||
{
|
||||
string locWarning = Loc.GetString(
|
||||
"chat-manager-max-message-length",
|
||||
("maxMessageLength", ChatMgr.MaxMessageLength));
|
||||
|
||||
AddLine(locWarning, ChatChannel.Server, Color.Orange);
|
||||
return;
|
||||
}
|
||||
|
||||
ChatMgr.OnChatBoxTextSubmitted(this, text, prefixChannel == 0 ? SelectedChannel : prefixChannel);
|
||||
}
|
||||
|
||||
Input.Clear();
|
||||
UpdateChannelSelectButton();
|
||||
|
||||
if (ReleaseFocusOnEnter)
|
||||
Input.ReleaseKeyboardFocus();
|
||||
}
|
||||
|
||||
public void Focus(ChatSelectChannel? channel = null)
|
||||
{
|
||||
var selectStart = Index.End;
|
||||
if (channel != null)
|
||||
{
|
||||
channel = MapLocalIfGhost(channel.Value);
|
||||
|
||||
// Channel not selectable, just do NOTHING (not even focus).
|
||||
if (!((ChatMgr.SelectableChannels & channel.Value) != 0))
|
||||
return;
|
||||
|
||||
var (_, text) = SplitInputContents();
|
||||
|
||||
var newPrefix = GetPrefixFromChannel(channel.Value);
|
||||
DebugTools.Assert(newPrefix != default, "Focus channel must have prefix!");
|
||||
|
||||
if (channel == SelectedChannel)
|
||||
{
|
||||
// New selected channel is just the selected channel,
|
||||
// just remove prefix (if any) and leave text unchanged.
|
||||
|
||||
Input.Text = text.ToString();
|
||||
selectStart = Index.Start;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Change prefix to new focused channel prefix and leave text unchanged.
|
||||
Input.Text = string.Concat(newPrefix.ToString(), " ", text.Span);
|
||||
selectStart = Index.FromStart(2);
|
||||
}
|
||||
|
||||
UpdateChannelSelectButton();
|
||||
}
|
||||
|
||||
Input.IgnoreNext = true;
|
||||
Input.GrabKeyboardFocus();
|
||||
|
||||
Input.CursorPosition = Input.Text.Length;
|
||||
Input.SelectionStart = selectStart.GetOffset(Input.Text.Length);
|
||||
}
|
||||
|
||||
private ChatSelectChannel MapLocalIfGhost(ChatSelectChannel channel)
|
||||
{
|
||||
if (channel == ChatSelectChannel.Local && ChatMgr.IsGhost)
|
||||
return ChatSelectChannel.Dead;
|
||||
|
||||
return channel;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Only needed to avoid the issue where right click on the button closes the popup
|
||||
/// but leaves the button highlighted.
|
||||
/// </summary>
|
||||
public sealed class ChannelSelectorButton : Button
|
||||
{
|
||||
public ChannelSelectorButton()
|
||||
{
|
||||
// needed so the popup is untoggled regardless of which key is pressed when hovering this button.
|
||||
// If we don't have this, then right clicking the button while it's toggled on will hide
|
||||
// the popup but keep the button toggled on
|
||||
Mode = ActionMode.Press;
|
||||
EnableAllKeybinds = true;
|
||||
}
|
||||
|
||||
protected override void KeyBindDown(GUIBoundKeyEventArgs args)
|
||||
{
|
||||
// needed since we need EnableAllKeybinds - don't double-send both UI click and Use
|
||||
if (args.Function == EngineKeyFunctions.Use)
|
||||
return;
|
||||
|
||||
base.KeyBindDown(args);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class FilterButton : ContainerButton
|
||||
{
|
||||
private static readonly Color ColorNormal = Color.FromHex("#7b7e9e");
|
||||
private static readonly Color ColorHovered = Color.FromHex("#9699bb");
|
||||
private static readonly Color ColorPressed = Color.FromHex("#789B8C");
|
||||
|
||||
private readonly TextureRect _textureRect;
|
||||
|
||||
public FilterButton()
|
||||
{
|
||||
var filterTexture = IoCManager.Resolve<IResourceCache>()
|
||||
.GetTexture("/Textures/Interface/Nano/filter.svg.96dpi.png");
|
||||
|
||||
// needed for same reason as ChannelSelectorButton
|
||||
Mode = ActionMode.Press;
|
||||
EnableAllKeybinds = true;
|
||||
|
||||
AddChild(
|
||||
(_textureRect = new TextureRect
|
||||
{
|
||||
Texture = filterTexture,
|
||||
HorizontalAlignment = HAlignment.Center,
|
||||
VerticalAlignment = VAlignment.Center
|
||||
})
|
||||
);
|
||||
|
||||
ToggleMode = true;
|
||||
}
|
||||
|
||||
protected override void KeyBindDown(GUIBoundKeyEventArgs args)
|
||||
{
|
||||
// needed since we need EnableAllKeybinds - don't double-send both UI click and Use
|
||||
if (args.Function == EngineKeyFunctions.Use) return;
|
||||
base.KeyBindDown(args);
|
||||
}
|
||||
|
||||
private void UpdateChildColors()
|
||||
{
|
||||
if (_textureRect == null) return;
|
||||
switch (DrawMode)
|
||||
{
|
||||
case DrawModeEnum.Normal:
|
||||
_textureRect.ModulateSelfOverride = ColorNormal;
|
||||
break;
|
||||
|
||||
case DrawModeEnum.Pressed:
|
||||
_textureRect.ModulateSelfOverride = ColorPressed;
|
||||
break;
|
||||
|
||||
case DrawModeEnum.Hover:
|
||||
_textureRect.ModulateSelfOverride = ColorHovered;
|
||||
break;
|
||||
|
||||
case DrawModeEnum.Disabled:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void DrawModeChanged()
|
||||
{
|
||||
base.DrawModeChanged();
|
||||
UpdateChildColors();
|
||||
}
|
||||
|
||||
protected override void StylePropertiesChanged()
|
||||
{
|
||||
base.StylePropertiesChanged();
|
||||
UpdateChildColors();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ChannelItemButton : Button
|
||||
{
|
||||
public readonly ChatSelectChannel Channel;
|
||||
|
||||
public ChannelItemButton(ChatSelectChannel channel)
|
||||
{
|
||||
Channel = channel;
|
||||
AddStyleClass(StyleNano.StyleClassChatChannelSelectorButton);
|
||||
Text = ChatBox.ChannelSelectorName(channel);
|
||||
|
||||
var prefix = ChatBox.GetPrefixFromChannel(channel);
|
||||
if (prefix != default)
|
||||
Text = Loc.GetString("hud-chatbox-select-name-prefixed", ("name", Text), ("prefix", prefix));
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ChannelFilterCheckbox : CheckBox
|
||||
{
|
||||
public readonly ChatChannel Channel;
|
||||
|
||||
public ChannelFilterCheckbox(ChatChannel channel, int? unreadCount)
|
||||
{
|
||||
Channel = channel;
|
||||
|
||||
UpdateText(unreadCount);
|
||||
}
|
||||
|
||||
private void UpdateText(int? unread)
|
||||
{
|
||||
var name = Loc.GetString($"hud-chatbox-channel-{Channel}");
|
||||
|
||||
if (unread > 0)
|
||||
// todo: proper fluent stuff here.
|
||||
name += " (" + (unread > 9 ? "9+" : unread) + ")";
|
||||
|
||||
Text = name;
|
||||
}
|
||||
|
||||
public void UpdateUnreadCount(int? unread)
|
||||
{
|
||||
UpdateText(unread);
|
||||
}
|
||||
}
|
||||
|
||||
public readonly struct ChatResizedEventArgs
|
||||
{
|
||||
/// new bottom that the chat rect is going to have in virtual pixels
|
||||
/// after the imminent relayout
|
||||
public readonly float NewBottom;
|
||||
|
||||
public ChatResizedEventArgs(float newBottom)
|
||||
{
|
||||
NewBottom = newBottom;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,7 @@
|
||||
using System;
|
||||
using Content.Client.Chat.Managers;
|
||||
using Content.Client.Viewport;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.Chat.UI
|
||||
@@ -48,6 +43,9 @@ namespace Content.Client.Chat.UI
|
||||
|
||||
public Vector2 ContentSize { get; private set; }
|
||||
|
||||
// man down
|
||||
public event Action<EntityUid, SpeechBubble>? OnDied;
|
||||
|
||||
public static SpeechBubble CreateSpeechBubble(SpeechType type, string text, EntityUid senderEntity, IEyeManager eyeManager, IChatManager chatManager, IEntityManager entityManager)
|
||||
{
|
||||
switch (type)
|
||||
@@ -148,7 +146,7 @@ namespace Content.Client.Chat.UI
|
||||
return;
|
||||
}
|
||||
|
||||
_chatManager.RemoveSpeechBubble(_senderEntity, this);
|
||||
OnDied?.Invoke(_senderEntity, this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -164,7 +162,6 @@ namespace Content.Client.Chat.UI
|
||||
}
|
||||
|
||||
public sealed class TextSpeechBubble : SpeechBubble
|
||||
|
||||
{
|
||||
public TextSpeechBubble(string text, EntityUid senderEntity, IEyeManager eyeManager, IChatManager chatManager, IEntityManager entityManager, string speechStyleClass)
|
||||
: base(text, senderEntity, eyeManager, chatManager, entityManager, speechStyleClass)
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
using Content.Client.HUD;
|
||||
using Content.Client.Verbs;
|
||||
using Content.Shared.CombatMode;
|
||||
using Content.Shared.Targeting;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
namespace Content.Client.CombatMode
|
||||
{
|
||||
@@ -14,7 +10,6 @@ namespace Content.Client.CombatMode
|
||||
public sealed class CombatModeComponent : SharedCombatModeComponent
|
||||
{
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IGameHud _gameHud = default!;
|
||||
|
||||
public override bool IsInCombatMode
|
||||
{
|
||||
@@ -36,14 +31,6 @@ namespace Content.Client.CombatMode
|
||||
}
|
||||
}
|
||||
|
||||
public void PlayerDetached() { _gameHud.CombatPanelVisible = false; }
|
||||
|
||||
public void PlayerAttached()
|
||||
{
|
||||
_gameHud.CombatPanelVisible = false; // TODO BOBBY SYSTEM Make the targeting doll actually do something.
|
||||
UpdateHud();
|
||||
}
|
||||
|
||||
private void UpdateHud()
|
||||
{
|
||||
if (Owner != _playerManager.LocalPlayer?.ControlledEntity)
|
||||
@@ -53,7 +40,6 @@ namespace Content.Client.CombatMode
|
||||
|
||||
var verbs = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<VerbSystem>();
|
||||
verbs.CloseAllMenus();
|
||||
_gameHud.TargetingZone = ActiveZone;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
using Content.Client.HUD;
|
||||
using Content.Shared.CombatMode;
|
||||
using Content.Shared.Targeting;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Input.Binding;
|
||||
@@ -12,17 +10,12 @@ namespace Content.Client.CombatMode
|
||||
[UsedImplicitly]
|
||||
public sealed class CombatModeSystem : SharedCombatModeSystem
|
||||
{
|
||||
[Dependency] private readonly IGameHud _gameHud = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_gameHud.OnTargetingZoneChanged = OnTargetingZoneChanged;
|
||||
|
||||
SubscribeLocalEvent<CombatModeComponent, PlayerAttachedEvent>((_, component, _) => component.PlayerAttached());
|
||||
SubscribeLocalEvent<CombatModeComponent, PlayerDetachedEvent>((_, component, _) => component.PlayerDetached());
|
||||
SubscribeLocalEvent<SharedCombatModeComponent, ComponentHandleState>(OnHandleState);
|
||||
}
|
||||
|
||||
@@ -34,7 +27,6 @@ namespace Content.Client.CombatMode
|
||||
component.IsInCombatMode = state.IsInCombatMode;
|
||||
component.ActiveZone = state.TargetingZone;
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
CommandBinds.Unregister<CombatModeSystem>();
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
using System;
|
||||
using Content.Client.Administration;
|
||||
using Content.Client.Administration.Systems;
|
||||
using Content.Client.UserInterface.Systems.Bwoink;
|
||||
using Content.Client.UserInterface.Systems.EscapeMenu;
|
||||
using Content.Shared.Administration;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Network;
|
||||
@@ -24,13 +27,14 @@ namespace Content.Client.Commands
|
||||
}
|
||||
if (args.Length == 0)
|
||||
{
|
||||
EntitySystem.Get<BwoinkSystem>().Open();
|
||||
IoCManager.Resolve<IUserInterfaceManager>().GetUIController<AHelpUIController>().Open();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Guid.TryParse(args[0], out var guid))
|
||||
{
|
||||
EntitySystem.Get<BwoinkSystem>().Open(new NetUserId(guid));
|
||||
var targetUser = new NetUserId(guid);
|
||||
IoCManager.Resolve<IUserInterfaceManager>().GetUIController<AHelpUIController>().Open(targetUser);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -1,22 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Client.HUD;
|
||||
using Content.Client.Resources;
|
||||
using Content.Client.UserInterface.Systems.MenuBar.Widgets;
|
||||
using Content.Shared.Construction.Prototypes;
|
||||
using Content.Shared.Construction.Steps;
|
||||
using Content.Shared.Tools;
|
||||
using Content.Shared.Tools.Components;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Placement;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Prototypes;
|
||||
using static Robust.Client.UserInterface.Controls.BaseButton;
|
||||
|
||||
namespace Content.Client.Construction.UI
|
||||
{
|
||||
@@ -30,8 +21,8 @@ namespace Content.Client.Construction.UI
|
||||
[Dependency] private readonly IEntitySystemManager _systemManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IPlacementManager _placementManager = default!;
|
||||
[Dependency] private readonly IUserInterfaceManager _uiManager = default!;
|
||||
|
||||
private readonly IGameHud _gameHud;
|
||||
private readonly IConstructionMenuView _constructionView;
|
||||
|
||||
private ConstructionSystem? _constructionSystem;
|
||||
@@ -39,10 +30,10 @@ namespace Content.Client.Construction.UI
|
||||
|
||||
private bool CraftingAvailable
|
||||
{
|
||||
get => _gameHud.CraftingButtonVisible;
|
||||
get => _uiManager.GetActiveUIWidget<GameTopMenuBar>().CraftingButton.Visible;
|
||||
set
|
||||
{
|
||||
_gameHud.CraftingButtonVisible = value;
|
||||
_uiManager.GetActiveUIWidget<GameTopMenuBar>().CraftingButton.Visible = value;
|
||||
if (!value)
|
||||
_constructionView.Close();
|
||||
}
|
||||
@@ -77,12 +68,10 @@ namespace Content.Client.Construction.UI
|
||||
/// Constructs a new instance of <see cref="ConstructionMenuPresenter" />.
|
||||
/// </summary>
|
||||
/// <param name="gameHud">GUI that is being presented to.</param>
|
||||
public ConstructionMenuPresenter(IGameHud gameHud)
|
||||
public ConstructionMenuPresenter()
|
||||
{
|
||||
// This is a lot easier than a factory
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
_gameHud = gameHud;
|
||||
_constructionView = new ConstructionMenu();
|
||||
|
||||
// This is required so that if we load after the system is initialized, we can bind to it immediately
|
||||
@@ -94,7 +83,7 @@ namespace Content.Client.Construction.UI
|
||||
|
||||
_placementManager.PlacementChanged += OnPlacementChanged;
|
||||
|
||||
_constructionView.OnClose += () => _gameHud.CraftingButtonDown = false;
|
||||
_constructionView.OnClose += () => _uiManager.GetActiveUIWidget<GameTopMenuBar>().CraftingButton.Pressed = false;
|
||||
_constructionView.ClearAllGhosts += (_, _) => _constructionSystem?.ClearAllGhosts();
|
||||
_constructionView.PopulateRecipes += OnViewPopulateRecipes;
|
||||
_constructionView.RecipeSelected += OnViewRecipeSelected;
|
||||
@@ -110,12 +99,11 @@ namespace Content.Client.Construction.UI
|
||||
PopulateCategories();
|
||||
OnViewPopulateRecipes(_constructionView, (string.Empty, string.Empty));
|
||||
|
||||
_gameHud.CraftingButtonToggled += OnHudCraftingButtonToggled;
|
||||
}
|
||||
|
||||
private void OnHudCraftingButtonToggled(bool b)
|
||||
public void OnHudCraftingButtonToggled(ButtonToggledEventArgs args)
|
||||
{
|
||||
WindowOpen = b;
|
||||
WindowOpen = args.Pressed;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -128,8 +116,6 @@ namespace Content.Client.Construction.UI
|
||||
_systemManager.SystemUnloaded -= OnSystemUnloaded;
|
||||
|
||||
_placementManager.PlacementChanged -= OnPlacementChanged;
|
||||
|
||||
_gameHud.CraftingButtonToggled -= OnHudCraftingButtonToggled;
|
||||
}
|
||||
|
||||
private void OnPlacementChanged(object? sender, EventArgs e)
|
||||
@@ -344,8 +330,11 @@ namespace Content.Client.Construction.UI
|
||||
system.ToggleCraftingWindow += SystemOnToggleMenu;
|
||||
system.CraftingAvailabilityChanged += SystemCraftingAvailabilityChanged;
|
||||
system.ConstructionGuideAvailable += SystemGuideAvailable;
|
||||
if (_uiManager.GetActiveUIWidgetOrNull<GameTopMenuBar>() != null)
|
||||
{
|
||||
CraftingAvailable = system.CraftingEnabled;
|
||||
}
|
||||
}
|
||||
|
||||
private void UnbindFromSystem()
|
||||
{
|
||||
@@ -362,6 +351,8 @@ namespace Content.Client.Construction.UI
|
||||
|
||||
private void SystemCraftingAvailabilityChanged(object? sender, CraftingAvailabilityChangedArgs e)
|
||||
{
|
||||
if (_uiManager.ActiveScreen == null)
|
||||
return;
|
||||
CraftingAvailable = e.Available;
|
||||
}
|
||||
|
||||
@@ -375,7 +366,7 @@ namespace Content.Client.Construction.UI
|
||||
if (IsAtFront)
|
||||
{
|
||||
WindowOpen = false;
|
||||
_gameHud.CraftingButtonDown = false; // This does not call CraftingButtonToggled
|
||||
_uiManager.GetActiveUIWidget<GameTopMenuBar>().CraftingButton.Pressed = false; // This does not call CraftingButtonToggled
|
||||
}
|
||||
else
|
||||
_constructionView.MoveToFront();
|
||||
@@ -383,7 +374,7 @@ namespace Content.Client.Construction.UI
|
||||
else
|
||||
{
|
||||
WindowOpen = true;
|
||||
_gameHud.CraftingButtonDown = true; // This does not call CraftingButtonToggled
|
||||
_uiManager.GetActiveUIWidget<GameTopMenuBar>().CraftingButton.Pressed = true; // This does not call CraftingButtonToggled
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,17 @@
|
||||
<ProjectReference Include="..\RobustToolbox\Robust.Client\Robust.Client.csproj" />
|
||||
<ProjectReference Include="..\Content.Shared\Content.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Update="UserInterface\Systems\MenuBar\Widgets\GameTopMenuBar.xaml.cs">
|
||||
<DependentUpon>GameTopMenuBar.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="UserInterface\Systems\HotBar\Widgets\HotbarGui.xaml.cs">
|
||||
<DependentUpon>HotbarGui.xaml</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="UserInterface\Systems\Inventory\Windows\StrippingWindow.xaml.cs">
|
||||
<DependentUpon>StrippingWindow.xaml</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
<Import Project="..\RobustToolbox\MSBuild\XamlIL.targets" />
|
||||
<Import Project="..\RobustToolbox\MSBuild\Robust.Analyzers.targets" />
|
||||
</Project>
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
using System;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.Cooldown
|
||||
{
|
||||
|
||||
public sealed class CooldownGraphic : Control
|
||||
{
|
||||
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly IPrototypeManager _protoMan = default!;
|
||||
|
||||
private readonly ShaderInstance _shader;
|
||||
@@ -51,6 +48,16 @@ namespace Content.Client.Cooldown
|
||||
handle.UseShader(null);
|
||||
}
|
||||
|
||||
}
|
||||
public void FromTime(TimeSpan start, TimeSpan end)
|
||||
{
|
||||
var duration = end - start;
|
||||
var curTime = _gameTiming.CurTime;
|
||||
var length = duration.TotalSeconds;
|
||||
var progress = (curTime - start).TotalSeconds / length;
|
||||
var ratio = (progress <= 1 ? (1 - progress) : (curTime - end).TotalSeconds * -5);
|
||||
|
||||
Progress = MathHelper.Clamp((float) ratio, -1, 1);
|
||||
Visible = ratio > -1f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
using Content.Client.Administration.Managers;
|
||||
using Content.Client.Changelog;
|
||||
using Content.Client.CharacterInterface;
|
||||
using Content.Client.Chat.Managers;
|
||||
using Content.Client.Options;
|
||||
using Content.Client.Eui;
|
||||
using Content.Client.Flash;
|
||||
using Content.Client.GhostKick;
|
||||
using Content.Client.HUD;
|
||||
using Content.Client.Info;
|
||||
using Content.Client.Input;
|
||||
using Content.Client.IoC;
|
||||
@@ -31,7 +29,6 @@ using Content.Shared.Markers;
|
||||
using Robust.Client;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.State;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.Configuration;
|
||||
@@ -42,14 +39,11 @@ using Robust.Shared.Configuration;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.Entry
|
||||
{
|
||||
public sealed class EntryPoint : GameClient
|
||||
{
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IBaseClient _baseClient = default!;
|
||||
[Dependency] private readonly IGameController _gameController = default!;
|
||||
[Dependency] private readonly IStateManager _stateManager = default!;
|
||||
@@ -76,7 +70,6 @@ namespace Content.Client.Entry
|
||||
[Dependency] private readonly GhostKickManager _ghostKick = default!;
|
||||
[Dependency] private readonly ExtendedDisconnectInformationManager _extendedDisconnectInformation = default!;
|
||||
[Dependency] private readonly PlayTimeTrackingManager _playTimeTracking = default!;
|
||||
[Dependency] private readonly IGameHud _gameHud = default!;
|
||||
|
||||
public const int NetBufferSizeOverride = 2;
|
||||
|
||||
@@ -168,9 +161,6 @@ namespace Content.Client.Entry
|
||||
_overlayManager.AddOverlay(new FlashOverlay());
|
||||
_overlayManager.AddOverlay(new RadiationPulseOverlay());
|
||||
|
||||
_baseClient.PlayerJoinedServer += SubscribePlayerAttachmentEvents;
|
||||
_baseClient.PlayerLeaveServer += UnsubscribePlayerAttachmentEvents;
|
||||
_gameHud.Initialize();
|
||||
_chatManager.Initialize();
|
||||
_clientPreferencesManager.Initialize();
|
||||
_euiManager.Initialize();
|
||||
@@ -194,56 +184,6 @@ namespace Content.Client.Entry
|
||||
SwitchToDefaultState();
|
||||
}
|
||||
|
||||
public override void Update(ModUpdateLevel level, FrameEventArgs frameEventArgs)
|
||||
{
|
||||
base.Update(level, frameEventArgs);
|
||||
|
||||
switch (level)
|
||||
{
|
||||
case ModUpdateLevel.FramePreEngine:
|
||||
// TODO: Turn IChatManager into an EntitySystem and remove the line below.
|
||||
IoCManager.Resolve<IChatManager>().FrameUpdate(frameEventArgs);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe events to the player manager after the player manager is set up
|
||||
/// </summary>
|
||||
/// <param name="sender"></param>
|
||||
/// <param name="args"></param>
|
||||
public void SubscribePlayerAttachmentEvents(object? sender, EventArgs args)
|
||||
{
|
||||
if (_playerManager.LocalPlayer != null)
|
||||
{
|
||||
_playerManager.LocalPlayer.EntityAttached += AttachPlayerToEntity;
|
||||
_playerManager.LocalPlayer.EntityDetached += DetachPlayerFromEntity;
|
||||
}
|
||||
}
|
||||
public void UnsubscribePlayerAttachmentEvents(object? sender, EventArgs args)
|
||||
{
|
||||
if (_playerManager.LocalPlayer != null)
|
||||
{
|
||||
_playerManager.LocalPlayer.EntityAttached -= AttachPlayerToEntity;
|
||||
_playerManager.LocalPlayer.EntityDetached -= DetachPlayerFromEntity;
|
||||
}
|
||||
}
|
||||
|
||||
public void AttachPlayerToEntity(EntityAttachedEventArgs eventArgs)
|
||||
{
|
||||
// TODO This is shitcode. Move this to an entity system, FOR FUCK'S SAKE
|
||||
_entityManager.AddComponent<CharacterInterfaceComponent>(eventArgs.NewEntity);
|
||||
}
|
||||
|
||||
public void DetachPlayerFromEntity(EntityDetachedEventArgs eventArgs)
|
||||
{
|
||||
// TODO This is shitcode. Move this to an entity system, FOR FUCK'S SAKE
|
||||
if (!_entityManager.Deleted(eventArgs.OldEntity))
|
||||
{
|
||||
_entityManager.RemoveComponent<CharacterInterfaceComponent>(eventArgs.OldEntity);
|
||||
}
|
||||
}
|
||||
|
||||
private void SwitchToDefaultState(bool disconnected = false)
|
||||
{
|
||||
// Fire off into state dependent on launcher or not.
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
using Content.Client.Alerts.UI;
|
||||
using Content.Client.Chat;
|
||||
using Content.Client.Chat.Managers;
|
||||
using Content.Client.Chat.UI;
|
||||
using Content.Client.Construction.UI;
|
||||
using Content.Client.Hands;
|
||||
using Content.Client.HUD;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Client.UserInterface.Screens;
|
||||
using Content.Client.Viewport;
|
||||
using Content.Client.Voting;
|
||||
using Content.Shared.Chat;
|
||||
using Content.Shared.CCVar;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Input;
|
||||
@@ -22,40 +16,26 @@ namespace Content.Client.Gameplay
|
||||
{
|
||||
public sealed class GameplayState : GameplayStateBase, IMainViewportState
|
||||
{
|
||||
public static readonly Vector2i ViewportSize = (EyeManager.PixelsPerMeter * 21, EyeManager.PixelsPerMeter * 15);
|
||||
|
||||
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
|
||||
[Dependency] private readonly IGameHud _gameHud = default!;
|
||||
[Dependency] private readonly IInputManager _inputManager = default!;
|
||||
[Dependency] private readonly IChatManager _chatManager = default!;
|
||||
[Dependency] private readonly IVoteManager _voteManager = default!;
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
[Dependency] private readonly IOverlayManager _overlayManager = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly IUserInterfaceManager _uiManager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
|
||||
|
||||
[ViewVariables] private ChatBox? _gameChat;
|
||||
private ConstructionMenuPresenter? _constructionMenu;
|
||||
private AlertsFramePresenter? _alertsFramePresenter;
|
||||
|
||||
protected override Type? LinkedScreenType => typeof(DefaultGameScreen);
|
||||
public static readonly Vector2i ViewportSize = (EyeManager.PixelsPerMeter * 21, EyeManager.PixelsPerMeter * 15);
|
||||
private FpsCounter _fpsCounter = default!;
|
||||
|
||||
public MainViewport Viewport { get; private set; } = default!;
|
||||
|
||||
public GameplayState()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
}
|
||||
|
||||
protected override void Startup()
|
||||
{
|
||||
base.Startup();
|
||||
|
||||
_gameChat = new HudChatBox {PreferredChannel = ChatSelectChannel.Local};
|
||||
|
||||
UserInterfaceManager.StateRoot.AddChild(_gameChat);
|
||||
LayoutContainer.SetAnchorAndMarginPreset(_gameChat, LayoutContainer.LayoutPreset.TopRight, margin: 10);
|
||||
LayoutContainer.SetAnchorAndMarginPreset(_gameChat, LayoutContainer.LayoutPreset.TopRight, margin: 10);
|
||||
LayoutContainer.SetMarginLeft(_gameChat, -475);
|
||||
LayoutContainer.SetMarginBottom(_gameChat, HudChatBox.InitialChatBottom);
|
||||
|
||||
_chatManager.ChatBoxOnResized(new ChatResizedEventArgs(HudChatBox.InitialChatBottom));
|
||||
|
||||
Viewport = new MainViewport
|
||||
{
|
||||
Viewport =
|
||||
@@ -63,25 +43,13 @@ namespace Content.Client.Gameplay
|
||||
ViewportSize = ViewportSize
|
||||
}
|
||||
};
|
||||
|
||||
_userInterfaceManager.StateRoot.AddChild(Viewport);
|
||||
UserInterfaceManager.StateRoot.AddChild(Viewport);
|
||||
LayoutContainer.SetAnchorPreset(Viewport, LayoutContainer.LayoutPreset.Wide);
|
||||
Viewport.SetPositionFirst();
|
||||
|
||||
_userInterfaceManager.StateRoot.AddChild(_gameHud.RootControl);
|
||||
_chatManager.SetChatBox(_gameChat);
|
||||
_voteManager.SetPopupContainer(_gameHud.VoteContainer);
|
||||
|
||||
ChatInput.SetupChatInputHandlers(_inputManager, _gameChat);
|
||||
|
||||
SetupPresenters();
|
||||
|
||||
_eyeManager.MainViewport = Viewport.Viewport;
|
||||
|
||||
_overlayManager.AddOverlay(new ShowHandItemOverlay());
|
||||
|
||||
_fpsCounter = new FpsCounter(_gameTiming);
|
||||
_userInterfaceManager.StateRoot.AddChild(_fpsCounter);
|
||||
UserInterfaceManager.PopupRoot.AddChild(_fpsCounter);
|
||||
_fpsCounter.Visible = _configurationManager.GetCVar(CCVars.HudFpsCounterVisible);
|
||||
_configurationManager.OnValueChanged(CCVars.HudFpsCounterVisible, (show) => { _fpsCounter.Visible = show; });
|
||||
}
|
||||
@@ -89,56 +57,13 @@ namespace Content.Client.Gameplay
|
||||
protected override void Shutdown()
|
||||
{
|
||||
_overlayManager.RemoveOverlay<ShowHandItemOverlay>();
|
||||
DisposePresenters();
|
||||
|
||||
base.Shutdown();
|
||||
|
||||
_gameChat?.Dispose();
|
||||
Viewport.Dispose();
|
||||
_gameHud.RootControl.Orphan();
|
||||
// Clear viewport to some fallback, whatever.
|
||||
_eyeManager.MainViewport = _userInterfaceManager.MainViewport;
|
||||
_eyeManager.MainViewport = UserInterfaceManager.MainViewport;
|
||||
_fpsCounter.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// All UI Presenters should be constructed in here.
|
||||
/// </summary>
|
||||
private void SetupPresenters()
|
||||
{
|
||||
// HUD
|
||||
_alertsFramePresenter = new AlertsFramePresenter();
|
||||
|
||||
// Windows
|
||||
_constructionMenu = new ConstructionMenuPresenter(_gameHud);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// All UI Presenters should be disposed in here.
|
||||
/// </summary>
|
||||
private void DisposePresenters()
|
||||
{
|
||||
// Windows
|
||||
_constructionMenu?.Dispose();
|
||||
|
||||
// HUD
|
||||
_alertsFramePresenter?.Dispose();
|
||||
}
|
||||
|
||||
internal static void FocusChat(ChatBox chat)
|
||||
{
|
||||
if (chat.UserInterfaceManager.KeyboardFocused != null)
|
||||
return;
|
||||
|
||||
chat.Focus();
|
||||
}
|
||||
|
||||
internal static void FocusChannel(ChatBox chat, ChatSelectChannel channel)
|
||||
{
|
||||
if (chat.UserInterfaceManager.KeyboardFocused != null)
|
||||
return;
|
||||
|
||||
chat.Focus(channel);
|
||||
_uiManager.ClearWindows();
|
||||
}
|
||||
|
||||
public override void FrameUpdate(FrameEventArgs e)
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using Content.Client.UserInterface.Systems.Ghost;
|
||||
using Content.Shared.Ghost;
|
||||
using Robust.Client.Player;
|
||||
|
||||
namespace Content.Client.Ghost
|
||||
{
|
||||
@@ -8,24 +6,6 @@ namespace Content.Client.Ghost
|
||||
[ComponentReference(typeof(SharedGhostComponent))]
|
||||
public sealed class GhostComponent : SharedGhostComponent
|
||||
{
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
|
||||
public GhostGui? Gui { get; set; }
|
||||
public bool IsAttached { get; set; }
|
||||
|
||||
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
|
||||
{
|
||||
base.HandleComponentState(curState, nextState);
|
||||
|
||||
if (curState is not GhostComponentState)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Owner == _playerManager.LocalPlayer?.ControlledEntity)
|
||||
{
|
||||
Gui?.Update();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
using Content.Client.HUD;
|
||||
using Content.Client.UserInterface.Systems.Ghost;
|
||||
using Content.Shared.Ghost;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Console;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Client.Ghost
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public sealed class GhostSystem : SharedGhostSystem
|
||||
{
|
||||
[Dependency] private readonly IClientConsoleHost _console = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IGameHud _gameHud = default!;
|
||||
|
||||
// Changes to this value are manually propagated.
|
||||
// No good way to get an event into the UI.
|
||||
public int AvailableGhostRoleCount { get; private set; } = 0;
|
||||
public int AvailableGhostRoleCount { get; private set; }
|
||||
|
||||
private bool _ghostVisibility = true;
|
||||
|
||||
@@ -38,23 +36,36 @@ namespace Content.Client.Ghost
|
||||
}
|
||||
}
|
||||
|
||||
public GhostComponent? Player => CompOrNull<GhostComponent>(_playerManager.LocalPlayer?.ControlledEntity);
|
||||
public bool IsGhost => Player != null;
|
||||
|
||||
public event Action<GhostComponent>? PlayerRemoved;
|
||||
public event Action<GhostComponent>? PlayerUpdated;
|
||||
public event Action<GhostComponent>? PlayerAttached;
|
||||
public event Action? PlayerDetached;
|
||||
public event Action<GhostWarpsResponseEvent>? GhostWarpsResponse;
|
||||
public event Action<GhostUpdateGhostRoleCountEvent>? GhostRoleCountUpdated;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<GhostComponent, ComponentInit>(OnGhostInit);
|
||||
SubscribeLocalEvent<GhostComponent, ComponentRemove>(OnGhostRemove);
|
||||
SubscribeLocalEvent<GhostComponent, ComponentHandleState>(OnGhostState);
|
||||
|
||||
SubscribeLocalEvent<GhostComponent, PlayerAttachedEvent>(OnGhostPlayerAttach);
|
||||
SubscribeLocalEvent<GhostComponent, PlayerDetachedEvent>(OnGhostPlayerDetach);
|
||||
|
||||
SubscribeLocalEvent<PlayerAttachedEvent>(OnPlayerAttach);
|
||||
|
||||
SubscribeNetworkEvent<GhostWarpsResponseEvent>(OnGhostWarpsResponse);
|
||||
SubscribeNetworkEvent<GhostUpdateGhostRoleCountEvent>(OnUpdateGhostRoleCount);
|
||||
}
|
||||
|
||||
private void OnGhostInit(EntityUid uid, GhostComponent component, ComponentInit args)
|
||||
{
|
||||
if (EntityManager.TryGetComponent(component.Owner, out SpriteComponent? sprite))
|
||||
if (TryComp(component.Owner, out SpriteComponent? sprite))
|
||||
{
|
||||
sprite.Visible = GhostVisibility;
|
||||
}
|
||||
@@ -62,55 +73,87 @@ namespace Content.Client.Ghost
|
||||
|
||||
private void OnGhostRemove(EntityUid uid, GhostComponent component, ComponentRemove args)
|
||||
{
|
||||
component.Gui?.Dispose();
|
||||
component.Gui = null;
|
||||
if (uid != _playerManager.LocalPlayer?.ControlledEntity)
|
||||
return;
|
||||
|
||||
if (component.IsAttached)
|
||||
{
|
||||
GhostVisibility = false;
|
||||
}
|
||||
|
||||
PlayerRemoved?.Invoke(component);
|
||||
}
|
||||
|
||||
private void OnGhostPlayerAttach(EntityUid uid, GhostComponent component, PlayerAttachedEvent playerAttachedEvent)
|
||||
{
|
||||
// I hate UI I hate UI I Hate UI
|
||||
if (component.Gui == null)
|
||||
{
|
||||
component.Gui = new GhostGui(component, this, EntityManager.EntityNetManager!);
|
||||
component.Gui.Update();
|
||||
}
|
||||
if (uid != _playerManager.LocalPlayer?.ControlledEntity)
|
||||
return;
|
||||
|
||||
_gameHud.HandsContainer.AddChild(component.Gui);
|
||||
GhostVisibility = true;
|
||||
component.IsAttached = true;
|
||||
PlayerAttached?.Invoke(component);
|
||||
}
|
||||
|
||||
private void OnGhostState(EntityUid uid, GhostComponent component, ref ComponentHandleState args)
|
||||
{
|
||||
if (uid != _playerManager.LocalPlayer?.ControlledEntity)
|
||||
return;
|
||||
|
||||
PlayerUpdated?.Invoke(component);
|
||||
}
|
||||
|
||||
private bool PlayerDetach(EntityUid uid)
|
||||
{
|
||||
if (uid != _playerManager.LocalPlayer?.ControlledEntity)
|
||||
return false;
|
||||
|
||||
GhostVisibility = false;
|
||||
PlayerDetached?.Invoke();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnGhostPlayerDetach(EntityUid uid, GhostComponent component, PlayerDetachedEvent args)
|
||||
{
|
||||
component.Gui?.Parent?.RemoveChild(component.Gui);
|
||||
GhostVisibility = false;
|
||||
if (PlayerDetach(uid))
|
||||
component.IsAttached = false;
|
||||
}
|
||||
|
||||
private void OnPlayerAttach(PlayerAttachedEvent ev)
|
||||
{
|
||||
if (!HasComp<GhostComponent>(ev.Entity))
|
||||
PlayerDetach(ev.Entity);
|
||||
}
|
||||
|
||||
private void OnGhostWarpsResponse(GhostWarpsResponseEvent msg)
|
||||
{
|
||||
var entity = _playerManager.LocalPlayer?.ControlledEntity;
|
||||
|
||||
if (entity == null ||
|
||||
!EntityManager.TryGetComponent(entity.Value, out GhostComponent? ghost))
|
||||
if (!IsGhost)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var window = ghost.Gui?.TargetWindow;
|
||||
|
||||
if (window != null)
|
||||
{
|
||||
window.UpdateWarps(msg.Warps);
|
||||
window.Populate();
|
||||
}
|
||||
GhostWarpsResponse?.Invoke(msg);
|
||||
}
|
||||
|
||||
private void OnUpdateGhostRoleCount(GhostUpdateGhostRoleCountEvent msg)
|
||||
{
|
||||
AvailableGhostRoleCount = msg.AvailableGhostRoles;
|
||||
foreach (var ghost in EntityManager.EntityQuery<GhostComponent>(true))
|
||||
ghost.Gui?.Update();
|
||||
GhostRoleCountUpdated?.Invoke(msg);
|
||||
}
|
||||
|
||||
public void RequestWarps()
|
||||
{
|
||||
RaiseNetworkEvent(new GhostWarpsRequestEvent());
|
||||
}
|
||||
|
||||
public void ReturnToBody()
|
||||
{
|
||||
var msg = new GhostReturnToBodyRequest();
|
||||
RaiseNetworkEvent(msg);
|
||||
}
|
||||
|
||||
public void OpenGhostRoles()
|
||||
{
|
||||
_console.RemoteExecuteCommand(null, "ghostroles");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,333 +0,0 @@
|
||||
using System;
|
||||
using Content.Client.HUD.UI;
|
||||
using Content.Client.Resources;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Shared.Input;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Content.Client.HUD;
|
||||
|
||||
public interface IButtonBarView
|
||||
{
|
||||
// Escape top button.
|
||||
bool EscapeButtonDown { get; set; }
|
||||
event Action<bool> EscapeButtonToggled;
|
||||
|
||||
// Character top button.
|
||||
bool CharacterButtonDown { get; set; }
|
||||
bool CharacterButtonVisible { get; set; }
|
||||
event Action<bool> CharacterButtonToggled;
|
||||
|
||||
// Inventory top button.
|
||||
bool InventoryButtonDown { get; set; }
|
||||
bool InventoryButtonVisible { get; set; }
|
||||
event Action<bool> InventoryButtonToggled;
|
||||
|
||||
// Crafting top button.
|
||||
bool CraftingButtonDown { get; set; }
|
||||
bool CraftingButtonVisible { get; set; }
|
||||
event Action<bool> CraftingButtonToggled;
|
||||
|
||||
// Actions top button.
|
||||
bool ActionsButtonDown { get; set; }
|
||||
bool ActionsButtonVisible { get; set; }
|
||||
event Action<bool> ActionsButtonToggled;
|
||||
|
||||
// Admin top button.
|
||||
bool AdminButtonDown { get; set; }
|
||||
bool AdminButtonVisible { get; set; }
|
||||
event Action<bool> AdminButtonToggled;
|
||||
|
||||
// Sandbox top button.
|
||||
bool SandboxButtonDown { get; set; }
|
||||
bool SandboxButtonVisible { get; set; }
|
||||
event Action<bool> SandboxButtonToggled;
|
||||
|
||||
// Info top button
|
||||
event Action InfoButtonPressed;
|
||||
void SetInfoRed(bool value);
|
||||
}
|
||||
|
||||
internal sealed partial class GameHud
|
||||
{
|
||||
private TopButton _buttonEscapeMenu = default!;
|
||||
private TopButton _buttonInfo = default!;
|
||||
private TopButton _buttonCharacterMenu = default!;
|
||||
private TopButton _buttonInventoryMenu = default!;
|
||||
private TopButton _buttonCraftingMenu = default!;
|
||||
private TopButton _buttonActionsMenu = default!;
|
||||
private TopButton _buttonAdminMenu = default!;
|
||||
private TopButton _buttonSandboxMenu = default!;
|
||||
|
||||
private BoxContainer GenerateButtonBar(IResourceCache resourceCache, IInputManager inputManager)
|
||||
{
|
||||
var topButtonsContainer = new BoxContainer
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Horizontal,
|
||||
SeparationOverride = 8
|
||||
};
|
||||
|
||||
LayoutContainer.SetAnchorAndMarginPreset(topButtonsContainer, LayoutContainer.LayoutPreset.TopLeft, margin: 10);
|
||||
|
||||
// the icon textures here should all have the same image height (32) but different widths, so in order to ensure
|
||||
// the buttons themselves are consistent widths we set a common custom min size
|
||||
Vector2 topMinSize = (42, 64);
|
||||
|
||||
// Escape
|
||||
{
|
||||
_buttonEscapeMenu = new TopButton(resourceCache.GetTexture("/Textures/Interface/hamburger.svg.192dpi.png"),
|
||||
EngineKeyFunctions.EscapeMenu, inputManager)
|
||||
{
|
||||
ToolTip = Loc.GetString("game-hud-open-escape-menu-button-tooltip"),
|
||||
MinSize = (70, 64),
|
||||
StyleClasses = { StyleBase.ButtonOpenRight }
|
||||
};
|
||||
|
||||
topButtonsContainer.AddChild(_buttonEscapeMenu);
|
||||
|
||||
_buttonEscapeMenu.OnToggled += args => EscapeButtonToggled?.Invoke(args.Pressed);
|
||||
}
|
||||
|
||||
// Character
|
||||
{
|
||||
_buttonCharacterMenu = new TopButton(resourceCache.GetTexture("/Textures/Interface/character.svg.192dpi.png"),
|
||||
ContentKeyFunctions.OpenCharacterMenu, inputManager)
|
||||
{
|
||||
ToolTip = Loc.GetString("game-hud-open-character-menu-button-tooltip"),
|
||||
MinSize = topMinSize,
|
||||
Visible = false,
|
||||
StyleClasses = { StyleBase.ButtonSquare }
|
||||
};
|
||||
|
||||
topButtonsContainer.AddChild(_buttonCharacterMenu);
|
||||
|
||||
_buttonCharacterMenu.OnToggled += args => CharacterButtonToggled?.Invoke(args.Pressed);
|
||||
}
|
||||
|
||||
// Inventory
|
||||
{
|
||||
_buttonInventoryMenu = new TopButton(resourceCache.GetTexture("/Textures/Interface/inventory.svg.192dpi.png"),
|
||||
ContentKeyFunctions.OpenInventoryMenu, inputManager)
|
||||
{
|
||||
ToolTip = Loc.GetString("game-hud-open-inventory-menu-button-tooltip"),
|
||||
MinSize = topMinSize,
|
||||
Visible = false,
|
||||
StyleClasses = { StyleBase.ButtonSquare }
|
||||
};
|
||||
|
||||
topButtonsContainer.AddChild(_buttonInventoryMenu);
|
||||
|
||||
_buttonInventoryMenu.OnToggled += args => InventoryButtonToggled?.Invoke(args.Pressed);
|
||||
}
|
||||
|
||||
// Crafting
|
||||
{
|
||||
_buttonCraftingMenu = new TopButton(resourceCache.GetTexture("/Textures/Interface/hammer.svg.192dpi.png"),
|
||||
ContentKeyFunctions.OpenCraftingMenu, inputManager)
|
||||
{
|
||||
ToolTip = Loc.GetString("game-hud-open-crafting-menu-button-tooltip"),
|
||||
MinSize = topMinSize,
|
||||
Visible = false,
|
||||
StyleClasses = { StyleBase.ButtonSquare }
|
||||
};
|
||||
|
||||
topButtonsContainer.AddChild(_buttonCraftingMenu);
|
||||
|
||||
_buttonCraftingMenu.OnToggled += args => CraftingButtonToggled?.Invoke(args.Pressed);
|
||||
}
|
||||
|
||||
// Actions
|
||||
{
|
||||
_buttonActionsMenu = new TopButton(resourceCache.GetTexture("/Textures/Interface/fist.svg.192dpi.png"),
|
||||
ContentKeyFunctions.OpenActionsMenu, inputManager)
|
||||
{
|
||||
ToolTip = Loc.GetString("game-hud-open-actions-menu-button-tooltip"),
|
||||
MinSize = topMinSize,
|
||||
Visible = false,
|
||||
StyleClasses = { StyleBase.ButtonSquare }
|
||||
};
|
||||
|
||||
topButtonsContainer.AddChild(_buttonActionsMenu);
|
||||
|
||||
_buttonActionsMenu.OnToggled += args => ActionsButtonToggled?.Invoke(args.Pressed);
|
||||
}
|
||||
|
||||
// Admin
|
||||
{
|
||||
_buttonAdminMenu = new TopButton(resourceCache.GetTexture("/Textures/Interface/gavel.svg.192dpi.png"),
|
||||
ContentKeyFunctions.OpenAdminMenu, inputManager)
|
||||
{
|
||||
ToolTip = Loc.GetString("game-hud-open-admin-menu-button-tooltip"),
|
||||
MinSize = topMinSize,
|
||||
Visible = false,
|
||||
StyleClasses = { StyleBase.ButtonSquare }
|
||||
};
|
||||
|
||||
topButtonsContainer.AddChild(_buttonAdminMenu);
|
||||
|
||||
_buttonAdminMenu.OnToggled += args => AdminButtonToggled?.Invoke(args.Pressed);
|
||||
}
|
||||
|
||||
// Sandbox
|
||||
{
|
||||
_buttonSandboxMenu = new TopButton(resourceCache.GetTexture("/Textures/Interface/sandbox.svg.192dpi.png"),
|
||||
ContentKeyFunctions.OpenSandboxWindow, inputManager)
|
||||
{
|
||||
ToolTip = Loc.GetString("game-hud-open-sandbox-menu-button-tooltip"),
|
||||
MinSize = topMinSize,
|
||||
Visible = false,
|
||||
StyleClasses = { StyleBase.ButtonSquare }
|
||||
};
|
||||
|
||||
topButtonsContainer.AddChild(_buttonSandboxMenu);
|
||||
|
||||
_buttonSandboxMenu.OnToggled += args => SandboxButtonToggled?.Invoke(args.Pressed);
|
||||
}
|
||||
|
||||
// Info Window
|
||||
{
|
||||
_buttonInfo = new TopButton(resourceCache.GetTexture("/Textures/Interface/info.svg.192dpi.png"),
|
||||
ContentKeyFunctions.OpenInfo, inputManager)
|
||||
{
|
||||
ToolTip = Loc.GetString("ui-options-function-open-info"),
|
||||
MinSize = topMinSize,
|
||||
StyleClasses = { StyleBase.ButtonOpenLeft },
|
||||
ToggleMode = false
|
||||
};
|
||||
|
||||
topButtonsContainer.AddChild(_buttonInfo);
|
||||
|
||||
_buttonInfo.OnPressed += args => InfoButtonPressed?.Invoke();
|
||||
}
|
||||
|
||||
return topButtonsContainer;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool EscapeButtonDown
|
||||
{
|
||||
get => _buttonEscapeMenu.Pressed;
|
||||
set => _buttonEscapeMenu.Pressed = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public event Action<bool>? EscapeButtonToggled;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CharacterButtonDown
|
||||
{
|
||||
get => _buttonCharacterMenu.Pressed;
|
||||
set => _buttonCharacterMenu.Pressed = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CharacterButtonVisible
|
||||
{
|
||||
get => _buttonCharacterMenu.Visible;
|
||||
set => _buttonCharacterMenu.Visible = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public event Action<bool>? CharacterButtonToggled;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool InventoryButtonDown
|
||||
{
|
||||
get => _buttonInventoryMenu.Pressed;
|
||||
set => _buttonInventoryMenu.Pressed = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool InventoryButtonVisible
|
||||
{
|
||||
get => _buttonInventoryMenu.Visible;
|
||||
set => _buttonInventoryMenu.Visible = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public event Action<bool>? InventoryButtonToggled;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CraftingButtonDown
|
||||
{
|
||||
get => _buttonCraftingMenu.Pressed;
|
||||
set => _buttonCraftingMenu.Pressed = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool CraftingButtonVisible
|
||||
{
|
||||
get => _buttonCraftingMenu.Visible;
|
||||
set => _buttonCraftingMenu.Visible = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public event Action<bool>? CraftingButtonToggled;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool ActionsButtonDown
|
||||
{
|
||||
get => _buttonActionsMenu.Pressed;
|
||||
set => _buttonActionsMenu.Pressed = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool ActionsButtonVisible
|
||||
{
|
||||
get => _buttonActionsMenu.Visible;
|
||||
set => _buttonActionsMenu.Visible = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public event Action<bool>? ActionsButtonToggled;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool AdminButtonDown
|
||||
{
|
||||
get => _buttonAdminMenu.Pressed;
|
||||
set => _buttonAdminMenu.Pressed = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool AdminButtonVisible
|
||||
{
|
||||
get => _buttonAdminMenu.Visible;
|
||||
set => _buttonAdminMenu.Visible = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public event Action<bool>? AdminButtonToggled;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool SandboxButtonDown
|
||||
{
|
||||
get => _buttonSandboxMenu.Pressed;
|
||||
set => _buttonSandboxMenu.Pressed = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool SandboxButtonVisible
|
||||
{
|
||||
get => _buttonSandboxMenu.Visible;
|
||||
set => _buttonSandboxMenu.Visible = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public event Action<bool>? SandboxButtonToggled;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event Action? InfoButtonPressed;
|
||||
|
||||
public void SetInfoRed(bool value)
|
||||
{
|
||||
if (value)
|
||||
_buttonInfo.StyleClasses.Add(TopButton.StyleClassRedTopButton);
|
||||
else
|
||||
_buttonInfo.StyleClasses.Remove(TopButton.StyleClassRedTopButton);
|
||||
}
|
||||
}
|
||||
@@ -1,274 +0,0 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Content.Client.HUD.UI;
|
||||
using Content.Client.Info;
|
||||
using Content.Client.Administration;
|
||||
using Content.Client.Administration.Systems;
|
||||
using Content.Client.Resources;
|
||||
using Content.Client.Targeting;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.HUD;
|
||||
using Content.Shared.Input;
|
||||
using Content.Shared.Targeting;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Input.Binding;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
using static Robust.Client.UserInterface.Controls.BoxContainer;
|
||||
using Control = Robust.Client.UserInterface.Control;
|
||||
using LC = Robust.Client.UserInterface.Controls.LayoutContainer;
|
||||
|
||||
namespace Content.Client.HUD
|
||||
{
|
||||
/// <summary>
|
||||
/// Responsible for laying out the default game HUD.
|
||||
/// </summary>
|
||||
public interface IGameHud : IButtonBarView
|
||||
{
|
||||
Control RootControl { get; }
|
||||
|
||||
Control HandsContainer { get; }
|
||||
Control SuspicionContainer { get; }
|
||||
Control BottomLeftInventoryQuickButtonContainer { get; }
|
||||
Control BottomRightInventoryQuickButtonContainer { get; }
|
||||
Control TopInventoryQuickButtonContainer { get; }
|
||||
|
||||
bool CombatPanelVisible { get; set; }
|
||||
TargetingZone TargetingZone { get; set; }
|
||||
Action<TargetingZone>? OnTargetingZoneChanged { get; set; }
|
||||
|
||||
Control VoteContainer { get; }
|
||||
|
||||
void AddTopNotification(TopNotification notification);
|
||||
|
||||
Texture GetHudTexture(string path);
|
||||
|
||||
bool ValidateHudTheme(int idx);
|
||||
|
||||
// Init logic.
|
||||
void Initialize();
|
||||
}
|
||||
|
||||
internal sealed partial class GameHud : IGameHud
|
||||
{
|
||||
private TargetingDoll _targetingDoll = default!;
|
||||
private BoxContainer _combatPanelContainer = default!;
|
||||
private BoxContainer _topNotificationContainer = default!;
|
||||
|
||||
[Dependency] private readonly IResourceCache _resourceCache = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IInputManager _inputManager = default!;
|
||||
[Dependency] private readonly INetConfigurationManager _configManager = default!;
|
||||
|
||||
public Control HandsContainer { get; private set; } = default!;
|
||||
public Control SuspicionContainer { get; private set; } = default!;
|
||||
public Control TopInventoryQuickButtonContainer { get; private set; } = default!;
|
||||
public Control BottomLeftInventoryQuickButtonContainer { get; private set; } = default!;
|
||||
public Control BottomRightInventoryQuickButtonContainer { get; private set; } = default!;
|
||||
|
||||
public bool CombatPanelVisible
|
||||
{
|
||||
get => _combatPanelContainer.Visible;
|
||||
set => _combatPanelContainer.Visible = value;
|
||||
}
|
||||
|
||||
public TargetingZone TargetingZone
|
||||
{
|
||||
get => _targetingDoll.ActiveZone;
|
||||
set => _targetingDoll.ActiveZone = value;
|
||||
}
|
||||
public Action<TargetingZone>? OnTargetingZoneChanged { get; set; }
|
||||
|
||||
public void AddTopNotification(TopNotification notification)
|
||||
{
|
||||
_topNotificationContainer.AddChild(notification);
|
||||
}
|
||||
|
||||
public bool ValidateHudTheme(int idx)
|
||||
{
|
||||
if (!_prototypeManager.TryIndex(idx.ToString(), out HudThemePrototype? _))
|
||||
{
|
||||
Logger.ErrorS("hud", "invalid HUD theme id {0}, using different theme",
|
||||
idx);
|
||||
var proto = _prototypeManager.EnumeratePrototypes<HudThemePrototype>().FirstOrDefault();
|
||||
if (proto == null)
|
||||
{
|
||||
throw new NullReferenceException("No valid HUD prototypes!");
|
||||
}
|
||||
var id = int.Parse(proto.ID);
|
||||
_configManager.SetCVar(CCVars.HudTheme, id);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public Texture GetHudTexture(string path)
|
||||
{
|
||||
var id = _configManager.GetCVar<int>("hud.theme");
|
||||
var dir = string.Empty;
|
||||
if (!_prototypeManager.TryIndex(id.ToString(), out HudThemePrototype? proto))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
dir = proto.Path;
|
||||
|
||||
var resourcePath = (new ResourcePath("/Textures/Interface/") / dir) / "Slots" / path;
|
||||
return _resourceCache.GetTexture(resourcePath);
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
RootControl = new LC { Name = "AAAAAAAAAAAAAAAAAAAAAA"};
|
||||
LC.SetAnchorPreset(RootControl, LC.LayoutPreset.Wide);
|
||||
|
||||
RootControl.AddChild(GenerateButtonBar(_resourceCache, _inputManager));
|
||||
|
||||
InventoryButtonToggled += down => TopInventoryQuickButtonContainer.Visible = down;
|
||||
InfoButtonPressed += () => ButtonInfoOnPressed();
|
||||
|
||||
_inputManager.SetInputCommand(ContentKeyFunctions.OpenInfo,
|
||||
InputCmdHandler.FromDelegate(s => ButtonInfoOnPressed()));
|
||||
|
||||
_combatPanelContainer = new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Vertical,
|
||||
HorizontalAlignment = Control.HAlignment.Left,
|
||||
VerticalAlignment = Control.VAlignment.Bottom,
|
||||
Children =
|
||||
{
|
||||
(_targetingDoll = new TargetingDoll(_resourceCache))
|
||||
}
|
||||
};
|
||||
|
||||
LC.SetGrowHorizontal(_combatPanelContainer, LC.GrowDirection.Begin);
|
||||
LC.SetGrowVertical(_combatPanelContainer, LC.GrowDirection.Begin);
|
||||
LC.SetAnchorAndMarginPreset(_combatPanelContainer, LC.LayoutPreset.BottomRight);
|
||||
LC.SetMarginBottom(_combatPanelContainer, -10f);
|
||||
|
||||
_targetingDoll.OnZoneChanged += args => OnTargetingZoneChanged?.Invoke(args);
|
||||
|
||||
var centerBottomContainer = new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Vertical,
|
||||
SeparationOverride = 5,
|
||||
HorizontalAlignment = Control.HAlignment.Center
|
||||
};
|
||||
LC.SetAnchorAndMarginPreset(centerBottomContainer, LC.LayoutPreset.CenterBottom);
|
||||
LC.SetGrowHorizontal(centerBottomContainer, LC.GrowDirection.Both);
|
||||
LC.SetGrowVertical(centerBottomContainer, LC.GrowDirection.Begin);
|
||||
LC.SetMarginBottom(centerBottomContainer, -10f);
|
||||
RootControl.AddChild(centerBottomContainer);
|
||||
|
||||
HandsContainer = new BoxContainer()
|
||||
{
|
||||
VerticalAlignment = Control.VAlignment.Bottom,
|
||||
HorizontalAlignment = Control.HAlignment.Center,
|
||||
Orientation = LayoutOrientation.Vertical,
|
||||
};
|
||||
BottomRightInventoryQuickButtonContainer = new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Horizontal,
|
||||
VerticalAlignment = Control.VAlignment.Bottom,
|
||||
HorizontalAlignment = Control.HAlignment.Right
|
||||
};
|
||||
BottomLeftInventoryQuickButtonContainer = new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Horizontal,
|
||||
VerticalAlignment = Control.VAlignment.Bottom,
|
||||
HorizontalAlignment = Control.HAlignment.Left
|
||||
};
|
||||
TopInventoryQuickButtonContainer = new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Horizontal,
|
||||
Visible = false,
|
||||
VerticalAlignment = Control.VAlignment.Bottom,
|
||||
HorizontalAlignment = Control.HAlignment.Center
|
||||
};
|
||||
var bottomRow = new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Horizontal,
|
||||
HorizontalAlignment = Control.HAlignment.Center
|
||||
|
||||
};
|
||||
bottomRow.AddChild(new Control {MinSize = (69, 0)}); //Padding (nice)
|
||||
bottomRow.AddChild(BottomLeftInventoryQuickButtonContainer);
|
||||
bottomRow.AddChild(HandsContainer);
|
||||
bottomRow.AddChild(BottomRightInventoryQuickButtonContainer);
|
||||
bottomRow.AddChild(new Control {MinSize = (1, 0)}); //Padding
|
||||
|
||||
|
||||
centerBottomContainer.AddChild(TopInventoryQuickButtonContainer);
|
||||
centerBottomContainer.AddChild(bottomRow);
|
||||
|
||||
SuspicionContainer = new Control
|
||||
{
|
||||
HorizontalAlignment = Control.HAlignment.Center
|
||||
};
|
||||
|
||||
var rightBottomContainer = new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Horizontal,
|
||||
SeparationOverride = 5
|
||||
};
|
||||
LC.SetAnchorAndMarginPreset(rightBottomContainer, LC.LayoutPreset.BottomRight);
|
||||
LC.SetGrowHorizontal(rightBottomContainer, LC.GrowDirection.Begin);
|
||||
LC.SetGrowVertical(rightBottomContainer, LC.GrowDirection.Begin);
|
||||
LC.SetMarginBottom(rightBottomContainer, -10f);
|
||||
LC.SetMarginRight(rightBottomContainer, -10f);
|
||||
RootControl.AddChild(rightBottomContainer);
|
||||
|
||||
rightBottomContainer.AddChild(_combatPanelContainer);
|
||||
|
||||
RootControl.AddChild(SuspicionContainer);
|
||||
|
||||
LC.SetAnchorAndMarginPreset(SuspicionContainer, LC.LayoutPreset.BottomLeft,
|
||||
margin: 10);
|
||||
LC.SetGrowHorizontal(SuspicionContainer, LC.GrowDirection.End);
|
||||
LC.SetGrowVertical(SuspicionContainer, LC.GrowDirection.Begin);
|
||||
|
||||
_topNotificationContainer = new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Vertical,
|
||||
MinSize = (600, 0)
|
||||
};
|
||||
RootControl.AddChild(_topNotificationContainer);
|
||||
LC.SetAnchorPreset(_topNotificationContainer, LC.LayoutPreset.CenterTop);
|
||||
LC.SetGrowHorizontal(_topNotificationContainer, LC.GrowDirection.Both);
|
||||
LC.SetGrowVertical(_topNotificationContainer, LC.GrowDirection.End);
|
||||
|
||||
VoteContainer = new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Vertical
|
||||
};
|
||||
RootControl.AddChild(VoteContainer);
|
||||
LC.SetAnchorPreset(VoteContainer, LC.LayoutPreset.TopLeft);
|
||||
LC.SetMarginLeft(VoteContainer, 180);
|
||||
LC.SetMarginTop(VoteContainer, 100);
|
||||
LC.SetGrowHorizontal(VoteContainer, LC.GrowDirection.End);
|
||||
LC.SetGrowVertical(VoteContainer, LC.GrowDirection.End);
|
||||
}
|
||||
|
||||
private void ButtonInfoOnPressed()
|
||||
{
|
||||
var bwoinkSystem = EntitySystem.Get<BwoinkSystem>();
|
||||
if (bwoinkSystem.IsOpen)
|
||||
{
|
||||
bwoinkSystem.Close();
|
||||
}
|
||||
else
|
||||
{
|
||||
bwoinkSystem.Open();
|
||||
}
|
||||
}
|
||||
|
||||
public Control RootControl { get; private set; } = default!;
|
||||
|
||||
public Control VoteContainer { get; private set; } = default!;
|
||||
}
|
||||
}
|
||||
@@ -1,230 +0,0 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.HUD.UI;
|
||||
|
||||
internal sealed class TopButton : ContainerButton
|
||||
{
|
||||
public const string StyleClassLabelTopButton = "topButtonLabel";
|
||||
public const string StyleClassRedTopButton = "topButtonLabel";
|
||||
private const float CustomTooltipDelay = 0.4f;
|
||||
|
||||
private static readonly Color ColorNormal = Color.FromHex("#7b7e9e");
|
||||
private static readonly Color ColorRedNormal = Color.FromHex("#FEFEFE");
|
||||
private static readonly Color ColorHovered = Color.FromHex("#9699bb");
|
||||
private static readonly Color ColorRedHovered = Color.FromHex("#FFFFFF");
|
||||
private static readonly Color ColorPressed = Color.FromHex("#789B8C");
|
||||
|
||||
private const float VertPad = 8f;
|
||||
private Color NormalColor => HasStyleClass(StyleClassRedTopButton) ? ColorRedNormal : ColorNormal;
|
||||
private Color HoveredColor => HasStyleClass(StyleClassRedTopButton) ? ColorRedHovered : ColorHovered;
|
||||
|
||||
private readonly TextureRect _textureRect;
|
||||
private readonly Label _label;
|
||||
private readonly BoundKeyFunction _function;
|
||||
private readonly IInputManager _inputManager;
|
||||
|
||||
public TopButton(Texture texture, BoundKeyFunction function, IInputManager inputManager)
|
||||
{
|
||||
_function = function;
|
||||
_inputManager = inputManager;
|
||||
TooltipDelay = CustomTooltipDelay;
|
||||
|
||||
AddChild(
|
||||
new BoxContainer
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Vertical,
|
||||
Children =
|
||||
{
|
||||
(_textureRect = new TextureRect
|
||||
{
|
||||
TextureScale = (0.5f, 0.5f),
|
||||
Texture = texture,
|
||||
HorizontalAlignment = HAlignment.Center,
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
VerticalExpand = true,
|
||||
Margin = new Thickness(0, VertPad),
|
||||
ModulateSelfOverride = NormalColor,
|
||||
Stretch = TextureRect.StretchMode.KeepCentered
|
||||
}),
|
||||
(_label = new Label
|
||||
{
|
||||
Text = ShortKeyName(_function),
|
||||
HorizontalAlignment = HAlignment.Center,
|
||||
ModulateSelfOverride = NormalColor,
|
||||
StyleClasses = {StyleClassLabelTopButton}
|
||||
})
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
ToggleMode = true;
|
||||
}
|
||||
|
||||
protected override void EnteredTree()
|
||||
{
|
||||
_inputManager.OnKeyBindingAdded += OnKeyBindingChanged;
|
||||
_inputManager.OnKeyBindingRemoved += OnKeyBindingChanged;
|
||||
_inputManager.OnInputModeChanged += OnKeyBindingChanged;
|
||||
}
|
||||
|
||||
protected override void ExitedTree()
|
||||
{
|
||||
_inputManager.OnKeyBindingAdded -= OnKeyBindingChanged;
|
||||
_inputManager.OnKeyBindingRemoved -= OnKeyBindingChanged;
|
||||
_inputManager.OnInputModeChanged -= OnKeyBindingChanged;
|
||||
}
|
||||
|
||||
|
||||
private void OnKeyBindingChanged(IKeyBinding obj)
|
||||
{
|
||||
_label.Text = ShortKeyName(_function);
|
||||
}
|
||||
|
||||
private void OnKeyBindingChanged()
|
||||
{
|
||||
_label.Text = ShortKeyName(_function);
|
||||
}
|
||||
|
||||
private string ShortKeyName(BoundKeyFunction keyFunction)
|
||||
{
|
||||
// need to use shortened key names so they fit in the buttons.
|
||||
return TryGetShortKeyName(keyFunction, out var name) ? Loc.GetString(name) : " ";
|
||||
}
|
||||
|
||||
private bool TryGetShortKeyName(BoundKeyFunction keyFunction, [NotNullWhen(true)] out string? name)
|
||||
{
|
||||
if (_inputManager.TryGetKeyBinding(keyFunction, out var binding))
|
||||
{
|
||||
// can't possibly fit a modifier key in the top button, so omit it
|
||||
var key = binding.BaseKey;
|
||||
if (binding.Mod1 != Keyboard.Key.Unknown || binding.Mod2 != Keyboard.Key.Unknown ||
|
||||
binding.Mod3 != Keyboard.Key.Unknown)
|
||||
{
|
||||
name = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
name = null;
|
||||
name = key switch
|
||||
{
|
||||
Keyboard.Key.Apostrophe => "'",
|
||||
Keyboard.Key.Comma => ",",
|
||||
Keyboard.Key.Delete => "Del",
|
||||
Keyboard.Key.Down => "Dwn",
|
||||
Keyboard.Key.Escape => "Esc",
|
||||
Keyboard.Key.Equal => "=",
|
||||
Keyboard.Key.Home => "Hom",
|
||||
Keyboard.Key.Insert => "Ins",
|
||||
Keyboard.Key.Left => "Lft",
|
||||
Keyboard.Key.Menu => "Men",
|
||||
Keyboard.Key.Minus => "-",
|
||||
Keyboard.Key.Num0 => "0",
|
||||
Keyboard.Key.Num1 => "1",
|
||||
Keyboard.Key.Num2 => "2",
|
||||
Keyboard.Key.Num3 => "3",
|
||||
Keyboard.Key.Num4 => "4",
|
||||
Keyboard.Key.Num5 => "5",
|
||||
Keyboard.Key.Num6 => "6",
|
||||
Keyboard.Key.Num7 => "7",
|
||||
Keyboard.Key.Num8 => "8",
|
||||
Keyboard.Key.Num9 => "9",
|
||||
Keyboard.Key.Pause => "||",
|
||||
Keyboard.Key.Period => ".",
|
||||
Keyboard.Key.Return => "Ret",
|
||||
Keyboard.Key.Right => "Rgt",
|
||||
Keyboard.Key.Slash => "/",
|
||||
Keyboard.Key.Space => "Spc",
|
||||
Keyboard.Key.Tab => "Tab",
|
||||
Keyboard.Key.Tilde => "~",
|
||||
Keyboard.Key.BackSlash => "\\",
|
||||
Keyboard.Key.BackSpace => "Bks",
|
||||
Keyboard.Key.LBracket => "[",
|
||||
Keyboard.Key.MouseButton4 => "M4",
|
||||
Keyboard.Key.MouseButton5 => "M5",
|
||||
Keyboard.Key.MouseButton6 => "M6",
|
||||
Keyboard.Key.MouseButton7 => "M7",
|
||||
Keyboard.Key.MouseButton8 => "M8",
|
||||
Keyboard.Key.MouseButton9 => "M9",
|
||||
Keyboard.Key.MouseLeft => "ML",
|
||||
Keyboard.Key.MouseMiddle => "MM",
|
||||
Keyboard.Key.MouseRight => "MR",
|
||||
Keyboard.Key.NumpadDecimal => "N.",
|
||||
Keyboard.Key.NumpadDivide => "N/",
|
||||
Keyboard.Key.NumpadEnter => "Ent",
|
||||
Keyboard.Key.NumpadMultiply => "*",
|
||||
Keyboard.Key.NumpadNum0 => "0",
|
||||
Keyboard.Key.NumpadNum1 => "1",
|
||||
Keyboard.Key.NumpadNum2 => "2",
|
||||
Keyboard.Key.NumpadNum3 => "3",
|
||||
Keyboard.Key.NumpadNum4 => "4",
|
||||
Keyboard.Key.NumpadNum5 => "5",
|
||||
Keyboard.Key.NumpadNum6 => "6",
|
||||
Keyboard.Key.NumpadNum7 => "7",
|
||||
Keyboard.Key.NumpadNum8 => "8",
|
||||
Keyboard.Key.NumpadNum9 => "9",
|
||||
Keyboard.Key.NumpadSubtract => "N-",
|
||||
Keyboard.Key.PageDown => "PgD",
|
||||
Keyboard.Key.PageUp => "PgU",
|
||||
Keyboard.Key.RBracket => "]",
|
||||
Keyboard.Key.SemiColon => ";",
|
||||
_ => DefaultShortKeyName(keyFunction)
|
||||
};
|
||||
return name != null;
|
||||
}
|
||||
|
||||
name = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private string? DefaultShortKeyName(BoundKeyFunction keyFunction)
|
||||
{
|
||||
var name = FormattedMessage.EscapeText(_inputManager.GetKeyFunctionButtonString(keyFunction));
|
||||
return name.Length > 3 ? null : name;
|
||||
}
|
||||
|
||||
protected override void StylePropertiesChanged()
|
||||
{
|
||||
// colors of children depend on style, so ensure we update when style is changed
|
||||
base.StylePropertiesChanged();
|
||||
UpdateChildColors();
|
||||
}
|
||||
|
||||
private void UpdateChildColors()
|
||||
{
|
||||
if (_label == null || _textureRect == null) return;
|
||||
switch (DrawMode)
|
||||
{
|
||||
case DrawModeEnum.Normal:
|
||||
_textureRect.ModulateSelfOverride = NormalColor;
|
||||
_label.ModulateSelfOverride = NormalColor;
|
||||
break;
|
||||
|
||||
case DrawModeEnum.Pressed:
|
||||
_textureRect.ModulateSelfOverride = ColorPressed;
|
||||
_label.ModulateSelfOverride = ColorPressed;
|
||||
break;
|
||||
|
||||
case DrawModeEnum.Hover:
|
||||
_textureRect.ModulateSelfOverride = HoveredColor;
|
||||
_label.ModulateSelfOverride = HoveredColor;
|
||||
break;
|
||||
|
||||
case DrawModeEnum.Disabled:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected override void DrawModeChanged()
|
||||
{
|
||||
base.DrawModeChanged();
|
||||
UpdateChildColors();
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
using Robust.Client.UserInterface;
|
||||
|
||||
namespace Content.Client.HUD.UI
|
||||
{
|
||||
public sealed class TopNotification : Control
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
using Content.Client.HUD;
|
||||
using Content.Client.Items.UI;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
|
||||
namespace Content.Client.Hands
|
||||
{
|
||||
public sealed class HandButton : ItemSlotButton
|
||||
{
|
||||
private bool _activeHand;
|
||||
private bool _highlighted;
|
||||
|
||||
public HandButton(int size, string textureName, string storageTextureName, IGameHud gameHud, Texture blockedTexture, HandLocation location) : base(size, textureName, storageTextureName, gameHud)
|
||||
{
|
||||
Location = location;
|
||||
|
||||
AddChild(Blocked = new TextureRect
|
||||
{
|
||||
Texture = blockedTexture,
|
||||
TextureScale = (2, 2),
|
||||
MouseFilter = MouseFilterMode.Stop,
|
||||
Visible = false
|
||||
});
|
||||
}
|
||||
|
||||
public HandLocation Location { get; }
|
||||
public TextureRect Blocked { get; }
|
||||
|
||||
public void SetActiveHand(bool active)
|
||||
{
|
||||
_activeHand = active;
|
||||
UpdateHighlight();
|
||||
}
|
||||
|
||||
public override void Highlight(bool highlight)
|
||||
{
|
||||
_highlighted = highlight;
|
||||
UpdateHighlight();
|
||||
}
|
||||
|
||||
private void UpdateHighlight()
|
||||
{
|
||||
// always stay highlighted if active
|
||||
base.Highlight(_activeHand || _highlighted);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,8 +15,6 @@ namespace Content.Client.Hands
|
||||
[DataField("showInHands")]
|
||||
public bool ShowInHands = true;
|
||||
|
||||
public HandsGui? Gui { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Data about the current sprite layers that the hand is contributing to the owner entity. Used for sprite in-hands.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Content.Client.Animations;
|
||||
using Content.Client.Hands.UI;
|
||||
using Content.Client.HUD;
|
||||
using Content.Client.Examine;
|
||||
using Content.Client.Verbs;
|
||||
using Content.Shared.Hands;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Hands.EntitySystems;
|
||||
@@ -21,24 +20,39 @@ namespace Content.Client.Hands.Systems
|
||||
public sealed class HandsSystem : SharedHandsSystem
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly IGameHud _gameHud = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
|
||||
[Dependency] private readonly SharedContainerSystem _containerSystem = default!;
|
||||
[Dependency] private readonly ExamineSystem _examine = default!;
|
||||
[Dependency] private readonly VerbSystem _verbs = default!;
|
||||
|
||||
public event Action<string, HandLocation>? OnPlayerAddHand;
|
||||
public event Action<string>? OnPlayerRemoveHand;
|
||||
public event Action<string?>? OnPlayerSetActiveHand;
|
||||
public event Action<HandsComponent>? OnPlayerHandsAdded;
|
||||
public event Action? OnPlayerHandsRemoved;
|
||||
public event Action<string, EntityUid>? OnPlayerItemAdded;
|
||||
public event Action<string, EntityUid>? OnPlayerItemRemoved;
|
||||
public event Action<string>? OnPlayerHandBlocked;
|
||||
public event Action<string>? OnPlayerHandUnblocked;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<SharedHandsComponent, EntRemovedFromContainerMessage>(HandleContainerModified);
|
||||
SubscribeLocalEvent<SharedHandsComponent, EntInsertedIntoContainerMessage>(HandleContainerModified);
|
||||
SubscribeLocalEvent<SharedHandsComponent, EntRemovedFromContainerMessage>(HandleItemRemoved);
|
||||
SubscribeLocalEvent<SharedHandsComponent, EntInsertedIntoContainerMessage>(HandleItemAdded);
|
||||
|
||||
SubscribeLocalEvent<HandsComponent, PlayerAttachedEvent>(HandlePlayerAttached);
|
||||
SubscribeLocalEvent<HandsComponent, PlayerDetachedEvent>(HandlePlayerDetached);
|
||||
SubscribeLocalEvent<HandsComponent, ComponentAdd>(HandleCompAdd);
|
||||
SubscribeLocalEvent<HandsComponent, ComponentRemove>(HandleCompRemove);
|
||||
SubscribeLocalEvent<HandsComponent, ComponentHandleState>(HandleComponentState);
|
||||
SubscribeLocalEvent<HandsComponent, VisualsChangedEvent>(OnVisualsChanged);
|
||||
|
||||
SubscribeNetworkEvent<PickupAnimationEvent>(HandlePickupAnimation);
|
||||
|
||||
OnHandSetActive += OnHandActivated;
|
||||
}
|
||||
|
||||
#region StateHandling
|
||||
@@ -49,30 +63,42 @@ namespace Content.Client.Hands.Systems
|
||||
|
||||
var handsModified = component.Hands.Count != state.Hands.Count;
|
||||
var manager = EnsureComp<ContainerManagerComponent>(uid);
|
||||
|
||||
if (handsModified)
|
||||
{
|
||||
List<Hand> addedHands = new();
|
||||
foreach (var hand in state.Hands)
|
||||
{
|
||||
if (component.Hands.TryAdd(hand.Name, hand))
|
||||
{
|
||||
hand.Container = _containerSystem.EnsureContainer<ContainerSlot>(uid, hand.Name, manager);
|
||||
handsModified = true;
|
||||
addedHands.Add(hand);
|
||||
}
|
||||
}
|
||||
|
||||
if (handsModified)
|
||||
{
|
||||
foreach (var name in component.Hands.Keys)
|
||||
{
|
||||
if (!state.HandNames.Contains(name))
|
||||
component.Hands.Remove(name);
|
||||
{
|
||||
RemoveHand(uid, name, component);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var hand in addedHands)
|
||||
{
|
||||
AddHand(uid, hand, component);
|
||||
}
|
||||
|
||||
component.SortedHands = new(state.HandNames);
|
||||
}
|
||||
|
||||
TrySetActiveHand(uid, state.ActiveHand, component);
|
||||
if (component.ActiveHand == null && state.ActiveHand == null)
|
||||
return; //edge case
|
||||
|
||||
if (uid == _playerManager.LocalPlayer?.ControlledEntity)
|
||||
UpdateGui();
|
||||
if (component.ActiveHand != null && state.ActiveHand != component.ActiveHand.Name)
|
||||
{
|
||||
SetActiveHand(uid, component.Hands[state.ActiveHand!], component);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
@@ -175,13 +201,64 @@ namespace Content.Client.Hands.Systems
|
||||
EntityManager.RaisePredictiveEvent(new RequestActivateInHandEvent(handName));
|
||||
}
|
||||
|
||||
#region visuals
|
||||
private void HandleContainerModified(EntityUid uid, SharedHandsComponent handComp, ContainerModifiedMessage args)
|
||||
public void UIInventoryExamine(string handName)
|
||||
{
|
||||
if (handComp.Hands.TryGetValue(args.Container.ID, out var hand))
|
||||
if (!TryGetPlayerHands(out var hands) ||
|
||||
!hands.Hands.TryGetValue(handName, out var hand) ||
|
||||
hand.HeldEntity is not { Valid: true } entity)
|
||||
{
|
||||
UpdateHandVisuals(uid, args.Entity, hand);
|
||||
return;
|
||||
}
|
||||
|
||||
_examine.DoExamine(entity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when a user clicks on the little "activation" icon in the hands GUI. This is currently only used
|
||||
/// by storage (backpacks, etc).
|
||||
/// </summary>
|
||||
public void UIHandOpenContextMenu(string handName)
|
||||
{
|
||||
if (!TryGetPlayerHands(out var hands) ||
|
||||
!hands.Hands.TryGetValue(handName, out var hand) ||
|
||||
hand.HeldEntity is not { Valid: true } entity)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_verbs.VerbMenu.OpenVerbMenu(entity);
|
||||
}
|
||||
|
||||
#region visuals
|
||||
|
||||
private void HandleItemAdded(EntityUid uid, SharedHandsComponent handComp, ContainerModifiedMessage args)
|
||||
{
|
||||
if (!handComp.Hands.TryGetValue(args.Container.ID, out var hand))
|
||||
return;
|
||||
UpdateHandVisuals(uid, args.Entity, hand);
|
||||
|
||||
if (uid != _playerManager.LocalPlayer?.ControlledEntity)
|
||||
return;
|
||||
|
||||
OnPlayerItemAdded?.Invoke(hand.Name, args.Entity);
|
||||
|
||||
if (HasComp<HandVirtualItemComponent>(args.Entity))
|
||||
OnPlayerHandBlocked?.Invoke(hand.Name);
|
||||
}
|
||||
|
||||
private void HandleItemRemoved(EntityUid uid, SharedHandsComponent handComp, ContainerModifiedMessage args)
|
||||
{
|
||||
if (!handComp.Hands.TryGetValue(args.Container.ID, out var hand))
|
||||
return;
|
||||
UpdateHandVisuals(uid, args.Entity, hand);
|
||||
|
||||
if (uid != _playerManager.LocalPlayer?.ControlledEntity)
|
||||
return;
|
||||
|
||||
OnPlayerItemRemoved?.Invoke(hand.Name, args.Entity);
|
||||
|
||||
if (HasComp<HandVirtualItemComponent>(args.Entity))
|
||||
OnPlayerHandUnblocked?.Invoke(hand.Name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -192,9 +269,6 @@ namespace Content.Client.Hands.Systems
|
||||
if (!Resolve(uid, ref handComp, ref sprite, false))
|
||||
return;
|
||||
|
||||
if (uid == _playerManager.LocalPlayer?.ControlledEntity)
|
||||
UpdateGui();
|
||||
|
||||
if (!handComp.ShowInHands)
|
||||
return;
|
||||
|
||||
@@ -206,6 +280,7 @@ namespace Content.Client.Hands.Systems
|
||||
{
|
||||
sprite.RemoveLayer(key);
|
||||
}
|
||||
|
||||
revealedLayers.Clear();
|
||||
}
|
||||
else
|
||||
@@ -267,52 +342,76 @@ namespace Content.Client.Hands.Systems
|
||||
#endregion
|
||||
|
||||
#region Gui
|
||||
public void UpdateGui(HandsComponent? hands = null)
|
||||
{
|
||||
if (hands == null && !TryGetPlayerHands(out hands) || hands.Gui == null)
|
||||
return;
|
||||
|
||||
var states = hands.Hands.Values
|
||||
.Select(hand => new GuiHand(hand.Name, hand.Location, hand.HeldEntity))
|
||||
.ToArray();
|
||||
|
||||
hands.Gui.Update(new HandsGuiState(states, hands.ActiveHand?.Name));
|
||||
}
|
||||
|
||||
public override bool TrySetActiveHand(EntityUid uid, string? value, SharedHandsComponent? handComp = null)
|
||||
{
|
||||
if (!base.TrySetActiveHand(uid, value, handComp))
|
||||
return false;
|
||||
|
||||
if (uid == _playerManager.LocalPlayer?.ControlledEntity)
|
||||
UpdateGui();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void HandlePlayerAttached(EntityUid uid, HandsComponent component, PlayerAttachedEvent args)
|
||||
{
|
||||
component.Gui = new HandsGui(component, this);
|
||||
_gameHud.HandsContainer.AddChild(component.Gui);
|
||||
component.Gui.SetPositionFirst();
|
||||
UpdateGui(component);
|
||||
if (_playerManager.LocalPlayer?.ControlledEntity == uid)
|
||||
OnPlayerHandsAdded?.Invoke(component);
|
||||
}
|
||||
|
||||
private static void HandlePlayerDetached(EntityUid uid, HandsComponent component, PlayerDetachedEvent args)
|
||||
private void HandlePlayerDetached(EntityUid uid, HandsComponent component, PlayerDetachedEvent args)
|
||||
{
|
||||
ClearGui(component);
|
||||
OnPlayerHandsRemoved?.Invoke();
|
||||
}
|
||||
|
||||
private static void HandleCompRemove(EntityUid uid, HandsComponent component, ComponentRemove args)
|
||||
private void HandleCompAdd(EntityUid uid, HandsComponent component, ComponentAdd args)
|
||||
{
|
||||
ClearGui(component);
|
||||
if (_playerManager.LocalPlayer?.ControlledEntity == uid)
|
||||
OnPlayerHandsAdded?.Invoke(component);
|
||||
}
|
||||
|
||||
private static void ClearGui(HandsComponent comp)
|
||||
private void HandleCompRemove(EntityUid uid, HandsComponent component, ComponentRemove args)
|
||||
{
|
||||
comp.Gui?.Orphan();
|
||||
comp.Gui = null;
|
||||
if (_playerManager.LocalPlayer?.ControlledEntity == uid)
|
||||
OnPlayerHandsRemoved?.Invoke();
|
||||
}
|
||||
#endregion
|
||||
|
||||
private void AddHand(EntityUid uid, Hand newHand, SharedHandsComponent? handsComp = null)
|
||||
{
|
||||
AddHand(uid, newHand.Name, newHand.Location, handsComp);
|
||||
}
|
||||
|
||||
public override void AddHand(EntityUid uid, string handName, HandLocation handLocation, SharedHandsComponent? handsComp = null)
|
||||
{
|
||||
base.AddHand(uid, handName, handLocation, handsComp);
|
||||
|
||||
if (uid == _playerManager.LocalPlayer?.ControlledEntity)
|
||||
OnPlayerAddHand?.Invoke(handName, handLocation);
|
||||
|
||||
if (handsComp == null)
|
||||
return;
|
||||
|
||||
if (handsComp.ActiveHand == null)
|
||||
SetActiveHand(uid, handsComp.Hands[handName], handsComp);
|
||||
}
|
||||
public override void RemoveHand(EntityUid uid, string handName, SharedHandsComponent? handsComp = null)
|
||||
{
|
||||
if (uid == _playerManager.LocalPlayer?.ControlledEntity && handsComp != null &&
|
||||
handsComp.Hands.ContainsKey(handName) && uid ==
|
||||
_playerManager.LocalPlayer?.ControlledEntity)
|
||||
{
|
||||
OnPlayerRemoveHand?.Invoke(handName);
|
||||
}
|
||||
|
||||
base.RemoveHand(uid, handName, handsComp);
|
||||
}
|
||||
|
||||
private void OnHandActivated(SharedHandsComponent? handsComponent)
|
||||
{
|
||||
if (handsComponent == null)
|
||||
return;
|
||||
|
||||
if (_playerManager.LocalPlayer?.ControlledEntity != handsComponent.Owner)
|
||||
return;
|
||||
|
||||
if (handsComponent.ActiveHand == null)
|
||||
{
|
||||
OnPlayerSetActiveHand?.Invoke(null);
|
||||
return;
|
||||
}
|
||||
|
||||
OnPlayerSetActiveHand?.Invoke(handsComponent.ActiveHand.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
<Control xmlns="https://spacestation14.io">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<Control Name="StatusContainer" />
|
||||
<BoxContainer Name="HandsContainer" Orientation="Horizontal" HorizontalAlignment="Center" />
|
||||
</BoxContainer>
|
||||
</Control>
|
||||
@@ -1,258 +0,0 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Client.Hands.Systems;
|
||||
using Content.Client.HUD;
|
||||
using Content.Client.Inventory;
|
||||
using Content.Client.Items.Managers;
|
||||
using Content.Client.Items.UI;
|
||||
using Content.Client.Resources;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.Hands.UI
|
||||
{
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class HandsGui : Control
|
||||
{
|
||||
[Dependency] private readonly IResourceCache _resourceCache = default!;
|
||||
[Dependency] private readonly IItemSlotManager _itemSlotManager = default!;
|
||||
[Dependency] private readonly IGameHud _gameHud = default!;
|
||||
[Dependency] private readonly INetConfigurationManager _configManager = default!;
|
||||
|
||||
private readonly HandsSystem _handsSystem;
|
||||
private readonly HandsComponent _handsComponent;
|
||||
|
||||
private string StorageTexture => "back.png";
|
||||
private Texture BlockedTexture => _resourceCache.GetTexture("/Textures/Interface/Default/blocked.png");
|
||||
|
||||
private ItemStatusPanel StatusPanel { get; }
|
||||
|
||||
[ViewVariables] private GuiHand[] _hands = Array.Empty<GuiHand>();
|
||||
|
||||
private string? ActiveHand { get; set; }
|
||||
|
||||
public HandsGui(HandsComponent hands, HandsSystem handsSystem)
|
||||
{
|
||||
_handsComponent = hands;
|
||||
_handsSystem = handsSystem;
|
||||
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
StatusPanel = ItemStatusPanel.FromSide(HandLocation.Middle);
|
||||
StatusContainer.AddChild(StatusPanel);
|
||||
StatusPanel.SetPositionFirst();
|
||||
}
|
||||
|
||||
protected override void EnteredTree()
|
||||
{
|
||||
base.EnteredTree();
|
||||
|
||||
_configManager.OnValueChanged(CCVars.HudTheme, UpdateHudTheme);
|
||||
}
|
||||
|
||||
protected override void ExitedTree()
|
||||
{
|
||||
base.ExitedTree();
|
||||
|
||||
_configManager.UnsubValueChanged(CCVars.HudTheme, UpdateHudTheme);
|
||||
}
|
||||
|
||||
public void Update(HandsGuiState state)
|
||||
{
|
||||
ActiveHand = state.ActiveHand;
|
||||
_hands = state.GuiHands;
|
||||
Array.Sort(_hands, HandOrderComparer.Instance);
|
||||
UpdateGui();
|
||||
}
|
||||
|
||||
private void UpdateGui()
|
||||
{
|
||||
HandsContainer.DisposeAllChildren();
|
||||
var entManager = IoCManager.Resolve<IEntityManager>();
|
||||
foreach (var hand in _hands)
|
||||
{
|
||||
var newButton = MakeHandButton(hand.HandLocation);
|
||||
HandsContainer.AddChild(newButton);
|
||||
hand.HandButton = newButton;
|
||||
|
||||
var handName = hand.Name;
|
||||
newButton.OnPressed += args => OnHandPressed(args, handName);
|
||||
newButton.OnStoragePressed += _ => OnStoragePressed(handName);
|
||||
|
||||
_itemSlotManager.SetItemSlot(newButton, hand.HeldItem);
|
||||
|
||||
// Show blocked overlay if hand is blocked.
|
||||
newButton.Blocked.Visible =
|
||||
hand.HeldItem != null && entManager.HasComponent<HandVirtualItemComponent>(hand.HeldItem.Value);
|
||||
}
|
||||
|
||||
if (TryGetActiveHand(out var activeHand))
|
||||
{
|
||||
activeHand.HandButton.SetActiveHand(true);
|
||||
StatusPanel.Update(activeHand.HeldItem);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnHandPressed(GUIBoundKeyEventArgs args, string handName)
|
||||
{
|
||||
if (args.Function == EngineKeyFunctions.UIClick)
|
||||
{
|
||||
_handsSystem.UIHandClick(_handsComponent, handName);
|
||||
}
|
||||
else if (TryGetHand(handName, out var hand))
|
||||
{
|
||||
_itemSlotManager.OnButtonPressed(args, hand.HeldItem);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnStoragePressed(string handName)
|
||||
{
|
||||
_handsSystem.UIHandActivate(handName);
|
||||
}
|
||||
|
||||
private bool TryGetActiveHand([NotNullWhen(true)] out GuiHand? activeHand)
|
||||
{
|
||||
TryGetHand(ActiveHand, out activeHand);
|
||||
return activeHand != null;
|
||||
}
|
||||
|
||||
private bool TryGetHand(string? handName, [NotNullWhen(true)] out GuiHand? foundHand)
|
||||
{
|
||||
foundHand = null;
|
||||
|
||||
if (handName == null)
|
||||
return false;
|
||||
|
||||
foreach (var hand in _hands)
|
||||
{
|
||||
if (hand.Name == handName)
|
||||
foundHand = hand;
|
||||
}
|
||||
|
||||
return foundHand != null;
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
|
||||
foreach (var hand in _hands)
|
||||
{
|
||||
_itemSlotManager.UpdateCooldown(hand.HandButton, hand.HeldItem);
|
||||
}
|
||||
}
|
||||
|
||||
private HandButton MakeHandButton(HandLocation buttonLocation)
|
||||
{
|
||||
var buttonTextureName = buttonLocation switch
|
||||
{
|
||||
HandLocation.Right => "hand_r.png",
|
||||
_ => "hand_l.png"
|
||||
};
|
||||
|
||||
return new HandButton(ClientInventorySystem.ButtonSize, buttonTextureName, StorageTexture, _gameHud, BlockedTexture, buttonLocation);
|
||||
}
|
||||
|
||||
private void UpdateHudTheme(int idx)
|
||||
{
|
||||
UpdateGui();
|
||||
}
|
||||
|
||||
private sealed class HandOrderComparer : IComparer<GuiHand>
|
||||
{
|
||||
public static readonly HandOrderComparer Instance = new();
|
||||
|
||||
public int Compare(GuiHand? x, GuiHand? y)
|
||||
{
|
||||
if (ReferenceEquals(x, y)) return 0;
|
||||
if (ReferenceEquals(null, y)) return 1;
|
||||
if (ReferenceEquals(null, x)) return -1;
|
||||
|
||||
var orderX = Map(x.HandLocation);
|
||||
var orderY = Map(y.HandLocation);
|
||||
|
||||
return orderX.CompareTo(orderY);
|
||||
|
||||
static int Map(HandLocation loc)
|
||||
{
|
||||
return loc switch
|
||||
{
|
||||
HandLocation.Left => 3,
|
||||
HandLocation.Middle => 2,
|
||||
HandLocation.Right => 1,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(loc), loc, null)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Info on a set of hands to be displayed.
|
||||
/// </summary>
|
||||
public sealed class HandsGuiState
|
||||
{
|
||||
/// <summary>
|
||||
/// The set of hands to be displayed.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public GuiHand[] GuiHands { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the currently active hand.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public string? ActiveHand { get; }
|
||||
|
||||
public HandsGuiState(GuiHand[] guiHands, string? activeHand = null)
|
||||
{
|
||||
GuiHands = guiHands;
|
||||
ActiveHand = activeHand;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Info on an individual hand to be displayed.
|
||||
/// </summary>
|
||||
public sealed class GuiHand
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of this hand.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Where this hand is located.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public HandLocation HandLocation { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The item being held in this hand.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public EntityUid? HeldItem { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The button in the gui associated with this hand. Assumed to be set by gui shortly after being received from the client HandsComponent.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public HandButton HandButton { get; set; } = default!;
|
||||
|
||||
public GuiHand(string name, HandLocation handLocation, EntityUid? heldItem)
|
||||
{
|
||||
Name = name;
|
||||
HandLocation = handLocation;
|
||||
HeldItem = heldItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using Content.Client.Changelog;
|
||||
using Content.Client.Credits;
|
||||
using Content.Client.Links;
|
||||
using Content.Client.UserInterface.Systems.EscapeMenu;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.IoC;
|
||||
@@ -33,6 +34,7 @@ namespace Content.Client.Info
|
||||
var wikiButton = new Button {Text = Loc.GetString("server-info-wiki-button")};
|
||||
wikiButton.OnPressed += args => uriOpener.OpenUri(UILinks.Wiki);
|
||||
var changelogButton = new ChangelogButton();
|
||||
changelogButton.OnPressed += args => UserInterfaceManager.GetUIController<ChangelogUIController>().ToggleWindow();
|
||||
buttons.AddChild(changelogButton);
|
||||
buttons.AddChild(rulesButton);
|
||||
buttons.AddChild(discordButton);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using Content.Client.Options.UI;
|
||||
using Content.Client.UserInterface.Systems.EscapeMenu;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface;
|
||||
@@ -20,7 +19,6 @@ namespace Content.Client.Info
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
|
||||
Title = Loc.GetString("ui-info-title");
|
||||
|
||||
var rootContainer = new TabContainer();
|
||||
@@ -50,12 +48,7 @@ namespace Content.Client.Info
|
||||
AddSection(tutorialList, Loc.GetString("ui-info-header-gameplay"), "Gameplay.txt", true);
|
||||
AddSection(tutorialList, Loc.GetString("ui-info-header-sandbox"), "Sandbox.txt", true);
|
||||
|
||||
infoControlSection.ControlsButton.OnPressed += OnOptionsPressed;
|
||||
}
|
||||
|
||||
private void OnOptionsPressed(BaseButton.ButtonEventArgs obj)
|
||||
{
|
||||
IoCManager.Resolve<IUserInterfaceManager>().GetUIController<OptionsUIController>().ToggleWindow();
|
||||
infoControlSection.ControlsButton.OnPressed += _ => UserInterfaceManager.GetUIController<OptionsUIController>().OpenWindow();
|
||||
}
|
||||
|
||||
private static void AddSection(Info info, Control control)
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace Content.Client.Input
|
||||
common.AddFunction(ContentKeyFunctions.CycleChatChannelForward);
|
||||
common.AddFunction(ContentKeyFunctions.CycleChatChannelBackward);
|
||||
common.AddFunction(ContentKeyFunctions.ExamineEntity);
|
||||
common.AddFunction(ContentKeyFunctions.OpenInfo);
|
||||
common.AddFunction(ContentKeyFunctions.OpenAHelp);
|
||||
common.AddFunction(ContentKeyFunctions.TakeScreenshot);
|
||||
common.AddFunction(ContentKeyFunctions.TakeScreenshotNoUI);
|
||||
common.AddFunction(ContentKeyFunctions.Point);
|
||||
@@ -63,25 +63,16 @@ namespace Content.Client.Input
|
||||
|
||||
// actions should be common (for ghosts, mobs, etc)
|
||||
common.AddFunction(ContentKeyFunctions.OpenActionsMenu);
|
||||
common.AddFunction(ContentKeyFunctions.Hotbar0);
|
||||
common.AddFunction(ContentKeyFunctions.Hotbar1);
|
||||
common.AddFunction(ContentKeyFunctions.Hotbar2);
|
||||
common.AddFunction(ContentKeyFunctions.Hotbar3);
|
||||
common.AddFunction(ContentKeyFunctions.Hotbar4);
|
||||
common.AddFunction(ContentKeyFunctions.Hotbar5);
|
||||
common.AddFunction(ContentKeyFunctions.Hotbar6);
|
||||
common.AddFunction(ContentKeyFunctions.Hotbar7);
|
||||
common.AddFunction(ContentKeyFunctions.Hotbar8);
|
||||
common.AddFunction(ContentKeyFunctions.Hotbar9);
|
||||
common.AddFunction(ContentKeyFunctions.Loadout1);
|
||||
common.AddFunction(ContentKeyFunctions.Loadout2);
|
||||
common.AddFunction(ContentKeyFunctions.Loadout3);
|
||||
common.AddFunction(ContentKeyFunctions.Loadout4);
|
||||
common.AddFunction(ContentKeyFunctions.Loadout5);
|
||||
common.AddFunction(ContentKeyFunctions.Loadout6);
|
||||
common.AddFunction(ContentKeyFunctions.Loadout7);
|
||||
common.AddFunction(ContentKeyFunctions.Loadout8);
|
||||
common.AddFunction(ContentKeyFunctions.Loadout9);
|
||||
|
||||
foreach (var boundKey in ContentKeyFunctions.GetHotbarBoundKeys())
|
||||
{
|
||||
common.AddFunction(boundKey);
|
||||
}
|
||||
|
||||
foreach (var boundKey in ContentKeyFunctions.GetLoadoutBoundKeys())
|
||||
{
|
||||
common.AddFunction(boundKey);
|
||||
}
|
||||
|
||||
var aghost = contexts.New("aghost", "common");
|
||||
aghost.AddFunction(EngineKeyFunctions.MoveUp);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using Content.Client.Items.UI;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.Inventory;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
@@ -19,24 +19,15 @@ namespace Content.Client.Inventory
|
||||
[Access(typeof(ClientInventorySystem))]
|
||||
public sealed class ClientInventoryComponent : InventoryComponent
|
||||
{
|
||||
public Control BottomLeftButtons = default!;
|
||||
public Control BottomRightButtons = default!;
|
||||
public Control TopQuickButtons = default!;
|
||||
|
||||
public DefaultWindow InventoryWindow = default!;
|
||||
|
||||
public readonly Dictionary<string, List<ItemSlotButton>> SlotButtons = new();
|
||||
|
||||
[ViewVariables]
|
||||
[DataField("speciesId")] public string? SpeciesId { get; set; }
|
||||
|
||||
[ViewVariables]
|
||||
public readonly Dictionary<string, ClientInventorySystem.SlotData> SlotData = new ();
|
||||
/// <summary>
|
||||
/// Data about the current layers that have been added to the players sprite due to the items in each equipment slot.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
[Access(typeof(ClientInventorySystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends
|
||||
public readonly Dictionary<string, HashSet<string>> VisualLayerKeys = new();
|
||||
|
||||
public bool AttachedToGameHud;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,76 +1,78 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Client.Clothing;
|
||||
using Content.Client.HUD;
|
||||
using Content.Shared.Input;
|
||||
using Content.Client.Items.Managers;
|
||||
using Content.Client.Items.UI;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Client.Examine;
|
||||
using Content.Client.Storage;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Client.Verbs;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Interaction.Events;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Inventory.Events;
|
||||
using Content.Shared.Item;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Input.Binding;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Content.Shared.Interaction.Events;
|
||||
|
||||
namespace Content.Client.Inventory
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public sealed class ClientInventorySystem : InventorySystem
|
||||
{
|
||||
[Dependency] private readonly IGameHud _gameHud = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _config = default!;
|
||||
[Dependency] private readonly IItemSlotManager _itemSlotManager = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
|
||||
[Dependency] private readonly ClothingVisualsSystem _clothingVisualsSystem = default!;
|
||||
[Dependency] private readonly ExamineSystem _examine = default!;
|
||||
[Dependency] private readonly VerbSystem _verbs = default!;
|
||||
|
||||
public const int ButtonSize = 64;
|
||||
private const int ButtonSeparation = 4;
|
||||
private const int RightSeparation = 2;
|
||||
public Action<SlotData>? EntitySlotUpdate = null;
|
||||
public Action<SlotData>? OnSlotAdded = null;
|
||||
public Action<SlotData>? OnSlotRemoved = null;
|
||||
public Action<ClientInventoryComponent>? OnLinkInventory = null;
|
||||
public Action? OnUnlinkInventory = null;
|
||||
public Action<SlotSpriteUpdate>? OnSpriteUpdate = null;
|
||||
|
||||
/// <summary>
|
||||
/// Stores delegates used to create controls for a given <see cref="InventoryTemplatePrototype"/>.
|
||||
/// </summary>
|
||||
private readonly
|
||||
Dictionary<string, Func<EntityUid, Dictionary<string, List<ItemSlotButton>>, (DefaultWindow window, Control bottomLeft, Control bottomRight, Control
|
||||
topQuick)>>
|
||||
_uiGenerateDelegates = new();
|
||||
private readonly Queue<(ClientInventoryComponent comp, EntityEventArgs args)> _equipEventsQueue = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
CommandBinds.Builder
|
||||
.Bind(ContentKeyFunctions.OpenInventoryMenu,
|
||||
InputCmdHandler.FromDelegate(_ => HandleOpenInventoryMenu()))
|
||||
.Register<ClientInventorySystem>();
|
||||
|
||||
SubscribeLocalEvent<ClientInventoryComponent, PlayerAttachedEvent>(OnPlayerAttached);
|
||||
SubscribeLocalEvent<ClientInventoryComponent, PlayerDetachedEvent>(OnPlayerDetached);
|
||||
|
||||
SubscribeLocalEvent<ClientInventoryComponent, ComponentInit>(OnInit);
|
||||
SubscribeLocalEvent<ClientInventoryComponent, ComponentShutdown>(OnShutdown);
|
||||
|
||||
SubscribeLocalEvent<ClientInventoryComponent, DidEquipEvent>(OnDidEquip);
|
||||
SubscribeLocalEvent<ClientInventoryComponent, DidUnequipEvent>(OnDidUnequip);
|
||||
SubscribeLocalEvent<ClientInventoryComponent, DidEquipEvent>((_, comp, args) =>
|
||||
_equipEventsQueue.Enqueue((comp, args)));
|
||||
SubscribeLocalEvent<ClientInventoryComponent, DidUnequipEvent>((_, comp, args) =>
|
||||
_equipEventsQueue.Enqueue((comp, args)));
|
||||
|
||||
SubscribeLocalEvent<ClothingComponent, UseInHandEvent>(OnUseInHand);
|
||||
}
|
||||
|
||||
_config.OnValueChanged(CCVars.HudTheme, UpdateHudTheme);
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
while (_equipEventsQueue.TryDequeue(out var tuple))
|
||||
{
|
||||
var (component, args) = tuple;
|
||||
|
||||
switch (args)
|
||||
{
|
||||
case DidEquipEvent equipped:
|
||||
OnDidEquip(component, equipped);
|
||||
break;
|
||||
case DidUnequipEvent unequipped:
|
||||
OnDidUnequip(component, unequipped);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException($"Received queued event of unknown type: {args.GetType()}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnUseInHand(EntityUid uid, ClothingComponent component, UseInHandEvent args)
|
||||
@@ -81,85 +83,60 @@ namespace Content.Client.Inventory
|
||||
QuickEquip(uid, component, args);
|
||||
}
|
||||
|
||||
private void OnDidUnequip(EntityUid uid, ClientInventoryComponent component, DidUnequipEvent args)
|
||||
private void OnDidUnequip(ClientInventoryComponent component, DidUnequipEvent args)
|
||||
{
|
||||
UpdateComponentUISlot(uid, args.Slot, null, component);
|
||||
}
|
||||
|
||||
private void OnDidEquip(EntityUid uid, ClientInventoryComponent component, DidEquipEvent args)
|
||||
{
|
||||
UpdateComponentUISlot(uid, args.Slot, args.Equipment, component);
|
||||
}
|
||||
|
||||
private void UpdateComponentUISlot(EntityUid uid, string slot, EntityUid? item, ClientInventoryComponent? component = null)
|
||||
{
|
||||
if (!Resolve(uid, ref component))
|
||||
UpdateSlot(args.Equipee, component, args.Slot);
|
||||
if (args.Equipee != _playerManager.LocalPlayer?.ControlledEntity)
|
||||
return;
|
||||
var update = new SlotSpriteUpdate(args.SlotGroup, args.Slot, null, false);
|
||||
OnSpriteUpdate?.Invoke(update);
|
||||
}
|
||||
|
||||
if (!component.SlotButtons.TryGetValue(slot, out var buttons))
|
||||
private void OnDidEquip(ClientInventoryComponent component, DidEquipEvent args)
|
||||
{
|
||||
UpdateSlot(args.Equipee, component, args.Slot);
|
||||
if (args.Equipee != _playerManager.LocalPlayer?.ControlledEntity)
|
||||
return;
|
||||
|
||||
UpdateUISlot(buttons, item);
|
||||
}
|
||||
|
||||
private void UpdateUISlot(List<ItemSlotButton> buttons, EntityUid? entity)
|
||||
{
|
||||
foreach (var button in buttons)
|
||||
{
|
||||
_itemSlotManager.SetItemSlot(button, entity);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPlayerDetached(EntityUid uid, ClientInventoryComponent component, PlayerDetachedEvent? args = null)
|
||||
{
|
||||
if(!component.AttachedToGameHud) return;
|
||||
|
||||
_gameHud.InventoryButtonVisible = false;
|
||||
_gameHud.BottomLeftInventoryQuickButtonContainer.RemoveChild(component.BottomLeftButtons);
|
||||
_gameHud.BottomRightInventoryQuickButtonContainer.RemoveChild(component.BottomRightButtons);
|
||||
_gameHud.TopInventoryQuickButtonContainer.RemoveChild(component.TopQuickButtons);
|
||||
component.AttachedToGameHud = false;
|
||||
var sprite = EntityManager.GetComponentOrNull<ISpriteComponent>(args.Equipment);
|
||||
var update = new SlotSpriteUpdate(args.SlotGroup, args.Slot, sprite,
|
||||
HasComp<ClientStorageComponent>(args.Equipment));
|
||||
OnSpriteUpdate?.Invoke(update);
|
||||
}
|
||||
|
||||
private void OnShutdown(EntityUid uid, ClientInventoryComponent component, ComponentShutdown args)
|
||||
{
|
||||
OnPlayerDetached(uid, component);
|
||||
if (component.Owner != _playerManager.LocalPlayer?.ControlledEntity)
|
||||
return;
|
||||
|
||||
OnUnlinkInventory?.Invoke();
|
||||
}
|
||||
|
||||
private void OnPlayerAttached(EntityUid uid, ClientInventoryComponent component, PlayerAttachedEvent args)
|
||||
{
|
||||
if(component.AttachedToGameHud) return;
|
||||
if (TryGetSlots(uid, out var definitions))
|
||||
{
|
||||
foreach (var definition in definitions)
|
||||
{
|
||||
if (!TryGetSlotContainer(uid, definition.Name, out var container, out _, component))
|
||||
continue;
|
||||
|
||||
_gameHud.InventoryButtonVisible = true;
|
||||
_gameHud.BottomLeftInventoryQuickButtonContainer.AddChild(component.BottomLeftButtons);
|
||||
_gameHud.BottomRightInventoryQuickButtonContainer.AddChild(component.BottomRightButtons);
|
||||
_gameHud.TopInventoryQuickButtonContainer.AddChild(component.TopQuickButtons);
|
||||
component.AttachedToGameHud = true;
|
||||
if (!component.SlotData.TryGetValue(definition.Name, out var data))
|
||||
{
|
||||
data = new SlotData(definition);
|
||||
component.SlotData[definition.Name] = data;
|
||||
}
|
||||
|
||||
private void UpdateHudTheme(int obj)
|
||||
{
|
||||
if (!_gameHud.ValidateHudTheme(obj))
|
||||
{
|
||||
return;
|
||||
data.Container = container;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var inventoryComponent in EntityManager.EntityQuery<ClientInventoryComponent>(true))
|
||||
{
|
||||
foreach (var slotButton in inventoryComponent.SlotButtons)
|
||||
{
|
||||
foreach (var btn in slotButton.Value)
|
||||
{
|
||||
btn.RefreshTextures(_gameHud);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (uid == _playerManager.LocalPlayer?.ControlledEntity)
|
||||
OnLinkInventory?.Invoke(component);
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
CommandBinds.Unregister<ClientInventorySystem>();
|
||||
_config.UnsubValueChanged(CCVars.HudTheme, UpdateHudTheme);
|
||||
base.Shutdown();
|
||||
}
|
||||
|
||||
@@ -167,28 +144,77 @@ namespace Content.Client.Inventory
|
||||
{
|
||||
_clothingVisualsSystem.InitClothing(uid, component);
|
||||
|
||||
if (!TryGetUIElements(uid, out var window, out var bottomLeft, out var bottomRight, out var topQuick,
|
||||
component))
|
||||
if (!_prototypeManager.TryIndex(component.TemplateId, out InventoryTemplatePrototype? invTemplate))
|
||||
return;
|
||||
|
||||
if (TryComp<ContainerManagerComponent>(uid, out var containerManager))
|
||||
foreach (var slot in invTemplate.Slots)
|
||||
{
|
||||
foreach (var (slot, buttons) in component.SlotButtons)
|
||||
TryAddSlotDef(uid, component, slot);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetSlotHighlight(EntityUid owner, ClientInventoryComponent component, string slotName, bool state)
|
||||
{
|
||||
if (!TryGetSlotEntity(uid, slot, out var entity, component, containerManager))
|
||||
continue;
|
||||
var oldData = component.SlotData[slotName];
|
||||
var newData = component.SlotData[slotName] = new SlotData(oldData, state);
|
||||
if (owner == _playerManager.LocalPlayer?.ControlledEntity)
|
||||
EntitySlotUpdate?.Invoke(newData);
|
||||
}
|
||||
|
||||
UpdateUISlot(buttons, entity);
|
||||
public void UpdateSlot(EntityUid owner, ClientInventoryComponent component, string slotName,
|
||||
bool? blocked = null, bool? highlight = null)
|
||||
{
|
||||
var oldData = component.SlotData[slotName];
|
||||
var newHighlight = oldData.Highlighted;
|
||||
var newBlocked = oldData.Blocked;
|
||||
|
||||
if (blocked != null)
|
||||
newBlocked = blocked.Value;
|
||||
|
||||
if (highlight != null)
|
||||
newHighlight = highlight.Value;
|
||||
|
||||
var newData = component.SlotData[slotName] =
|
||||
new SlotData(component.SlotData[slotName], newHighlight, newBlocked);
|
||||
if (owner == _playerManager.LocalPlayer?.ControlledEntity)
|
||||
EntitySlotUpdate?.Invoke(newData);
|
||||
}
|
||||
|
||||
public bool TryAddSlotDef(EntityUid owner, ClientInventoryComponent component, SlotDefinition newSlotDef)
|
||||
{
|
||||
SlotData newSlotData = newSlotDef; //convert to slotData
|
||||
if (!component.SlotData.TryAdd(newSlotDef.Name, newSlotData))
|
||||
return false;
|
||||
|
||||
|
||||
if (owner == _playerManager.LocalPlayer?.ControlledEntity)
|
||||
OnSlotAdded?.Invoke(newSlotData);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void RemoveSlotDef(EntityUid owner, ClientInventoryComponent component, SlotData slotData)
|
||||
{
|
||||
if (component.SlotData.Remove(slotData.SlotName))
|
||||
{
|
||||
if (owner == _playerManager.LocalPlayer?.ControlledEntity)
|
||||
OnSlotRemoved?.Invoke(slotData);
|
||||
}
|
||||
}
|
||||
|
||||
component.InventoryWindow = window;
|
||||
component.BottomLeftButtons = bottomLeft;
|
||||
component.BottomRightButtons = bottomRight;
|
||||
component.TopQuickButtons = topQuick;
|
||||
public void RemoveSlotDef(EntityUid owner, ClientInventoryComponent component, string slotName)
|
||||
{
|
||||
if (!component.SlotData.TryGetValue(slotName, out var slotData))
|
||||
return;
|
||||
|
||||
component.SlotData.Remove(slotName);
|
||||
|
||||
if (owner == _playerManager.LocalPlayer?.ControlledEntity)
|
||||
OnSlotRemoved?.Invoke(slotData);
|
||||
}
|
||||
|
||||
private void HoverInSlotButton(EntityUid uid, string slot, ItemSlotButton button, InventoryComponent? inventoryComponent = null, SharedHandsComponent? hands = null)
|
||||
// TODO hud refactor This should also live in a UI Controller
|
||||
private void HoverInSlotButton(EntityUid uid, string slot, SlotControl control,
|
||||
InventoryComponent? inventoryComponent = null, SharedHandsComponent? hands = null)
|
||||
{
|
||||
if (!Resolve(uid, ref inventoryComponent))
|
||||
return;
|
||||
@@ -201,158 +227,99 @@ namespace Content.Client.Inventory
|
||||
|
||||
if (!TryGetSlotContainer(uid, slot, out var containerSlot, out var slotDef, inventoryComponent))
|
||||
return;
|
||||
|
||||
_itemSlotManager.HoverInSlot(button, heldEntity,
|
||||
CanEquip(uid, heldEntity, slot, out _, slotDef, inventoryComponent) &&
|
||||
containerSlot.CanInsert(heldEntity, EntityManager));
|
||||
}
|
||||
|
||||
private void HandleSlotButtonPressed(EntityUid uid, string slot, ItemSlotButton button,
|
||||
GUIBoundKeyEventArgs args)
|
||||
public void UIInventoryActivate(string slot)
|
||||
{
|
||||
if (TryGetSlotEntity(uid, slot, out var itemUid) && _itemSlotManager.OnButtonPressed(args, itemUid.Value))
|
||||
return;
|
||||
|
||||
if (args.Function != EngineKeyFunctions.UIClick)
|
||||
return;
|
||||
|
||||
// only raise event if either itemUid is not null, or the user is holding something
|
||||
if (itemUid != null || TryComp(uid, out SharedHandsComponent? hands) && hands.ActiveHandEntity != null)
|
||||
EntityManager.RaisePredictiveEvent(new UseSlotNetworkMessage(slot));
|
||||
}
|
||||
|
||||
private bool TryGetUIElements(EntityUid uid, [NotNullWhen(true)] out DefaultWindow? invWindow,
|
||||
[NotNullWhen(true)] out Control? invBottomLeft, [NotNullWhen(true)] out Control? invBottomRight,
|
||||
[NotNullWhen(true)] out Control? invTopQuick, ClientInventoryComponent? component = null)
|
||||
public void UIInventoryStorageActivate(string slot)
|
||||
{
|
||||
invWindow = null;
|
||||
invBottomLeft = null;
|
||||
invBottomRight = null;
|
||||
invTopQuick = null;
|
||||
EntityManager.RaisePredictiveEvent(new OpenSlotStorageNetworkMessage(slot));
|
||||
}
|
||||
|
||||
if (!Resolve(uid, ref component))
|
||||
return false;
|
||||
|
||||
if(!_prototypeManager.TryIndex<InventoryTemplatePrototype>(component.TemplateId, out var template))
|
||||
return false;
|
||||
|
||||
if (!_uiGenerateDelegates.TryGetValue(component.TemplateId, out var genfunc))
|
||||
public void UIInventoryExamine(string slot, EntityUid uid)
|
||||
{
|
||||
_uiGenerateDelegates[component.TemplateId] = genfunc = (entityUid, list) =>
|
||||
{
|
||||
var window = new DefaultWindow()
|
||||
{
|
||||
Title = Loc.GetString("human-inventory-window-title"),
|
||||
Resizable = false
|
||||
};
|
||||
window.OnClose += () =>
|
||||
{
|
||||
_gameHud.InventoryButtonDown = false;
|
||||
_gameHud.TopInventoryQuickButtonContainer.Visible = false;
|
||||
};
|
||||
var windowContents = new LayoutContainer
|
||||
{
|
||||
MinSize = (ButtonSize * 4 + ButtonSeparation * 3 + RightSeparation,
|
||||
ButtonSize * 4 + ButtonSeparation * 3)
|
||||
};
|
||||
window.Contents.AddChild(windowContents);
|
||||
|
||||
ItemSlotButton GetButton(SlotDefinition definition, string textureBack)
|
||||
{
|
||||
var btn = new ItemSlotButton(ButtonSize, $"{definition.TextureName}.png", textureBack,
|
||||
_gameHud)
|
||||
{
|
||||
OnStoragePressed = (e) =>
|
||||
{
|
||||
if (e.Function != EngineKeyFunctions.UIClick &&
|
||||
e.Function != ContentKeyFunctions.ActivateItemInWorld)
|
||||
if (!TryGetSlotEntity(uid, slot, out var item))
|
||||
return;
|
||||
RaiseNetworkEvent(new OpenSlotStorageNetworkMessage(definition.Name));
|
||||
}
|
||||
};
|
||||
btn.OnHover = (_) =>
|
||||
{
|
||||
HoverInSlotButton(entityUid, definition.Name, btn);
|
||||
};
|
||||
btn.OnPressed = (e) =>
|
||||
{
|
||||
HandleSlotButtonPressed(entityUid, definition.Name, btn, e);
|
||||
};
|
||||
return btn;
|
||||
|
||||
_examine.DoExamine(item.Value);
|
||||
}
|
||||
|
||||
void AddButton(SlotDefinition definition, Vector2i position)
|
||||
public void UIInventoryOpenContextMenu(string slot, EntityUid uid)
|
||||
{
|
||||
var button = GetButton(definition, "back.png");
|
||||
LayoutContainer.SetPosition(button, position);
|
||||
windowContents.AddChild(button);
|
||||
if (!list.ContainsKey(definition.Name))
|
||||
list[definition.Name] = new();
|
||||
list[definition.Name].Add(button);
|
||||
if (!TryGetSlotEntity(uid, slot, out var item))
|
||||
return;
|
||||
|
||||
_verbs.VerbMenu.OpenVerbMenu(item.Value);
|
||||
}
|
||||
|
||||
void AddHUDButton(BoxContainer container, SlotDefinition definition)
|
||||
public void UIInventoryActivateItem(string slot, EntityUid uid)
|
||||
{
|
||||
var button = GetButton(definition, "back.png");
|
||||
container.AddChild(button);
|
||||
if (!list.ContainsKey(definition.Name))
|
||||
list[definition.Name] = new();
|
||||
list[definition.Name].Add(button);
|
||||
if (!TryGetSlotEntity(uid, slot, out var item))
|
||||
return;
|
||||
|
||||
EntityManager.EntityNetManager?.SendSystemNetworkMessage(
|
||||
new InteractInventorySlotEvent(item.Value, altInteract: false));
|
||||
}
|
||||
|
||||
var topQuick = new BoxContainer
|
||||
public void UIInventoryAltActivateItem(string slot, EntityUid uid)
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Horizontal,
|
||||
SeparationOverride = 5
|
||||
};
|
||||
var bottomRight = new BoxContainer
|
||||
if (!TryGetSlotEntity(uid, slot, out var item))
|
||||
return;
|
||||
|
||||
EntityManager.RaisePredictiveEvent(new InteractInventorySlotEvent(item.Value, altInteract: true));
|
||||
}
|
||||
|
||||
public sealed class SlotData
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Horizontal,
|
||||
SeparationOverride = 5
|
||||
};
|
||||
var bottomLeft = new BoxContainer
|
||||
public readonly SlotDefinition SlotDef;
|
||||
public EntityUid? HeldEntity => Container?.ContainedEntity;
|
||||
public bool Blocked;
|
||||
public bool Highlighted;
|
||||
public ContainerSlot? Container;
|
||||
public bool HasSlotGroup => SlotDef.SlotGroup != "Default";
|
||||
public Vector2i ButtonOffset => SlotDef.UIWindowPosition;
|
||||
public string SlotName => SlotDef.Name;
|
||||
public bool ShowInWindow => SlotDef.ShowInWindow;
|
||||
public string SlotGroup => SlotDef.SlotGroup;
|
||||
public string SlotDisplayName => SlotDef.DisplayName;
|
||||
public string TextureName => "Slots/" + SlotDef.TextureName;
|
||||
|
||||
public SlotData(SlotDefinition slotDef, ContainerSlot? container = null, bool highlighted = false,
|
||||
bool blocked = false)
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Horizontal,
|
||||
SeparationOverride = 5
|
||||
};
|
||||
SlotDef = slotDef;
|
||||
Highlighted = highlighted;
|
||||
Blocked = blocked;
|
||||
Container = container;
|
||||
}
|
||||
|
||||
const int sizep = (ButtonSize + ButtonSeparation);
|
||||
|
||||
foreach (var slotDefinition in template.Slots)
|
||||
public SlotData(SlotData oldData, bool highlighted = false, bool blocked = false)
|
||||
{
|
||||
switch (slotDefinition.UIContainer)
|
||||
SlotDef = oldData.SlotDef;
|
||||
Highlighted = highlighted;
|
||||
Container = oldData.Container;
|
||||
Blocked = blocked;
|
||||
}
|
||||
|
||||
public static implicit operator SlotData(SlotDefinition s)
|
||||
{
|
||||
case SlotUIContainer.BottomLeft:
|
||||
AddHUDButton(bottomLeft, slotDefinition);
|
||||
break;
|
||||
case SlotUIContainer.BottomRight:
|
||||
AddHUDButton(bottomRight, slotDefinition);
|
||||
break;
|
||||
case SlotUIContainer.Top:
|
||||
AddHUDButton(topQuick, slotDefinition);
|
||||
break;
|
||||
return new SlotData(s);
|
||||
}
|
||||
|
||||
AddButton(slotDefinition, slotDefinition.UIWindowPosition * sizep);
|
||||
}
|
||||
|
||||
return (window, bottomLeft, bottomRight, topQuick);
|
||||
};
|
||||
}
|
||||
|
||||
var res = genfunc(uid, component.SlotButtons);
|
||||
invWindow = res.window;
|
||||
invBottomLeft = res.bottomLeft;
|
||||
invBottomRight = res.bottomRight;
|
||||
invTopQuick = res.topQuick;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private void HandleOpenInventoryMenu()
|
||||
public static implicit operator SlotDefinition(SlotData s)
|
||||
{
|
||||
_gameHud.InventoryButtonDown = !_gameHud.InventoryButtonDown;
|
||||
_gameHud.TopInventoryQuickButtonContainer.Visible = _gameHud.InventoryButtonDown;
|
||||
return s.SlotDef;
|
||||
}
|
||||
}
|
||||
|
||||
public readonly record struct SlotSpriteUpdate(
|
||||
string Group,
|
||||
string Name,
|
||||
ISpriteComponent? Sprite,
|
||||
bool ShowStorage
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,7 @@ using Content.Client.Clickable;
|
||||
using Content.Client.Options;
|
||||
using Content.Client.Eui;
|
||||
using Content.Client.GhostKick;
|
||||
using Content.Client.HUD;
|
||||
using Content.Client.Info;
|
||||
using Content.Client.Items.Managers;
|
||||
using Content.Client.Launcher;
|
||||
using Content.Client.Module;
|
||||
using Content.Client.Parallax.Managers;
|
||||
@@ -27,12 +25,10 @@ namespace Content.Client.IoC
|
||||
{
|
||||
public static void Register()
|
||||
{
|
||||
IoCManager.Register<IGameHud, GameHud>();
|
||||
IoCManager.Register<IParallaxManager, ParallaxManager>();
|
||||
IoCManager.Register<IChatManager, ChatManager>();
|
||||
IoCManager.Register<IModuleManager, ClientModuleManager>();
|
||||
IoCManager.Register<IClientPreferencesManager, ClientPreferencesManager>();
|
||||
IoCManager.Register<IItemSlotManager, ItemSlotManager>();
|
||||
IoCManager.Register<IStylesheetManager, StylesheetManager>();
|
||||
IoCManager.Register<IScreenshotHook, ScreenshotHook>();
|
||||
IoCManager.Register<IClickMapManager, ClickMapManager>();
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
using System;
|
||||
using Content.Client.Items.UI;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Client.Items.Managers
|
||||
{
|
||||
public interface IItemSlotManager
|
||||
{
|
||||
bool OnButtonPressed(GUIBoundKeyEventArgs args, EntityUid? item);
|
||||
void UpdateCooldown(ItemSlotButton? cooldownTexture, EntityUid? entity);
|
||||
bool SetItemSlot(ItemSlotButton button, EntityUid? entity);
|
||||
void HoverInSlot(ItemSlotButton button, EntityUid? entity, bool fits);
|
||||
event Action<EntitySlotHighlightedEventArgs>? EntityHighlightedUpdated;
|
||||
bool IsHighlighted(EntityUid? uid);
|
||||
|
||||
/// <summary>
|
||||
/// Highlight all slot controls that contain the specified entity.
|
||||
/// </summary>
|
||||
/// <param name="uid">The UID of the entity to highlight.</param>
|
||||
/// <seealso cref="UnHighlightEntity"/>
|
||||
void HighlightEntity(EntityUid uid);
|
||||
|
||||
/// <summary>
|
||||
/// Remove highlighting for the specified entity.
|
||||
/// </summary>
|
||||
/// <param name="uid">The UID of the entity to unhighlight.</param>
|
||||
/// <seealso cref="HighlightEntity"/>
|
||||
void UnHighlightEntity(EntityUid uid);
|
||||
}
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Content.Client.Examine;
|
||||
using Content.Client.Items.UI;
|
||||
using Content.Client.Storage;
|
||||
using Content.Client.Verbs;
|
||||
using Content.Shared.Cooldown;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Input;
|
||||
using Content.Shared.Interaction;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.Items.Managers
|
||||
{
|
||||
public sealed class ItemSlotManager : IItemSlotManager
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
|
||||
private readonly HashSet<EntityUid> _highlightEntities = new();
|
||||
|
||||
public event Action<EntitySlotHighlightedEventArgs>? EntityHighlightedUpdated;
|
||||
|
||||
public bool SetItemSlot(ItemSlotButton button, EntityUid? entity)
|
||||
{
|
||||
if (entity == null)
|
||||
{
|
||||
button.SpriteView.Sprite = null;
|
||||
button.StorageButton.Visible = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
ISpriteComponent? sprite;
|
||||
if (_entityManager.TryGetComponent(entity, out HandVirtualItemComponent? virtPull)
|
||||
&& _entityManager.TryGetComponent(virtPull.BlockingEntity, out ISpriteComponent? pulledSprite))
|
||||
{
|
||||
sprite = pulledSprite;
|
||||
}
|
||||
else if (!_entityManager.TryGetComponent(entity, out sprite))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
button.ClearHover();
|
||||
button.SpriteView.Sprite = sprite;
|
||||
button.StorageButton.Visible = _entityManager.HasComponent<ClientStorageComponent>(entity);
|
||||
}
|
||||
|
||||
button.Entity = entity ?? default;
|
||||
|
||||
// im lazy
|
||||
button.UpdateSlotHighlighted();
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool OnButtonPressed(GUIBoundKeyEventArgs args, EntityUid? item)
|
||||
{
|
||||
if (item == null)
|
||||
return false;
|
||||
|
||||
if (args.Function == ContentKeyFunctions.ExamineEntity)
|
||||
{
|
||||
_entitySystemManager.GetEntitySystem<ExamineSystem>()
|
||||
.DoExamine(item.Value);
|
||||
}
|
||||
else if (args.Function == EngineKeyFunctions.UseSecondary)
|
||||
{
|
||||
_entitySystemManager.GetEntitySystem<VerbSystem>().VerbMenu.OpenVerbMenu(item.Value);
|
||||
}
|
||||
else if (args.Function == ContentKeyFunctions.ActivateItemInWorld)
|
||||
{
|
||||
_entityManager.EntityNetManager?.SendSystemNetworkMessage(new InteractInventorySlotEvent(item.Value, altInteract: false));
|
||||
}
|
||||
else if (args.Function == ContentKeyFunctions.AltActivateItemInWorld)
|
||||
{
|
||||
_entityManager.RaisePredictiveEvent(new InteractInventorySlotEvent(item.Value, altInteract: true));
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
args.Handle();
|
||||
return true;
|
||||
}
|
||||
|
||||
public void UpdateCooldown(ItemSlotButton? button, EntityUid? entity)
|
||||
{
|
||||
var cooldownDisplay = button?.CooldownDisplay;
|
||||
|
||||
if (cooldownDisplay == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (entity == null || _entityManager.Deleted(entity) ||
|
||||
!_entityManager.TryGetComponent(entity, out ItemCooldownComponent? cooldown) ||
|
||||
!cooldown.CooldownStart.HasValue ||
|
||||
!cooldown.CooldownEnd.HasValue)
|
||||
{
|
||||
cooldownDisplay.Visible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var start = cooldown.CooldownStart.Value;
|
||||
var end = cooldown.CooldownEnd.Value;
|
||||
|
||||
var length = (end - start).TotalSeconds;
|
||||
var progress = (_gameTiming.CurTime - start).TotalSeconds / length;
|
||||
var ratio = (progress <= 1 ? (1 - progress) : (_gameTiming.CurTime - end).TotalSeconds * -5);
|
||||
|
||||
cooldownDisplay.Progress = MathHelper.Clamp((float) ratio, -1, 1);
|
||||
cooldownDisplay.Visible = ratio > -1f;
|
||||
}
|
||||
|
||||
public void HoverInSlot(ItemSlotButton button, EntityUid? entity, bool fits)
|
||||
{
|
||||
if (entity == null || !button.MouseIsHovering)
|
||||
{
|
||||
button.ClearHover();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_entityManager.HasComponent<SpriteComponent>(entity))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Set green / red overlay at 50% transparency
|
||||
var hoverEntity = _entityManager.SpawnEntity("hoverentity", MapCoordinates.Nullspace);
|
||||
var hoverSprite = _entityManager.GetComponent<SpriteComponent>(hoverEntity);
|
||||
hoverSprite.CopyFrom(_entityManager.GetComponent<SpriteComponent>(entity.Value));
|
||||
hoverSprite.Color = fits ? new Color(0, 255, 0, 127) : new Color(255, 0, 0, 127);
|
||||
|
||||
button.HoverSpriteView.Sprite = hoverSprite;
|
||||
}
|
||||
|
||||
public bool IsHighlighted(EntityUid? uid)
|
||||
{
|
||||
if (uid == null) return false;
|
||||
return _highlightEntities.Contains(uid.Value);
|
||||
}
|
||||
|
||||
public void HighlightEntity(EntityUid uid)
|
||||
{
|
||||
if (!_highlightEntities.Add(uid))
|
||||
return;
|
||||
|
||||
EntityHighlightedUpdated?.Invoke(new EntitySlotHighlightedEventArgs(uid, true));
|
||||
}
|
||||
|
||||
public void UnHighlightEntity(EntityUid uid)
|
||||
{
|
||||
if (!_highlightEntities.Remove(uid))
|
||||
return;
|
||||
|
||||
EntityHighlightedUpdated?.Invoke(new EntitySlotHighlightedEventArgs(uid, false));
|
||||
}
|
||||
}
|
||||
|
||||
public readonly struct EntitySlotHighlightedEventArgs
|
||||
{
|
||||
public EntitySlotHighlightedEventArgs(EntityUid entity, bool newHighlighted)
|
||||
{
|
||||
Entity = entity;
|
||||
NewHighlighted = newHighlighted;
|
||||
}
|
||||
|
||||
public EntityUid Entity { get; }
|
||||
public bool NewHighlighted { get; }
|
||||
}
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
using System;
|
||||
using Content.Client.Cooldown;
|
||||
using Content.Client.HUD;
|
||||
using Content.Client.Items.Managers;
|
||||
using Content.Client.Stylesheets;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Items.UI
|
||||
{
|
||||
[Virtual]
|
||||
public class ItemSlotButton : Control, IEntityEventSubscriber
|
||||
{
|
||||
private const string HighlightShader = "SelectionOutlineInrange";
|
||||
|
||||
[Dependency] private readonly IItemSlotManager _itemSlotManager = default!;
|
||||
|
||||
public EntityUid? Entity { get; set; }
|
||||
public TextureRect Button { get; }
|
||||
public SpriteView SpriteView { get; }
|
||||
public SpriteView HoverSpriteView { get; }
|
||||
public TextureButton StorageButton { get; }
|
||||
public CooldownGraphic CooldownDisplay { get; }
|
||||
|
||||
public Action<GUIBoundKeyEventArgs>? OnPressed { get; set; }
|
||||
public Action<GUIBoundKeyEventArgs>? OnStoragePressed { get; set; }
|
||||
public Action<GUIMouseHoverEventArgs>? OnHover { get; set; }
|
||||
|
||||
public bool EntityHover => HoverSpriteView.Sprite != null;
|
||||
public bool MouseIsHovering;
|
||||
|
||||
private readonly PanelContainer _highlightRect;
|
||||
|
||||
private string _textureName;
|
||||
private string _storageTextureName;
|
||||
|
||||
public ItemSlotButton(int size, string textureName, string storageTextureName, IGameHud gameHud)
|
||||
{
|
||||
_textureName = textureName;
|
||||
_storageTextureName = storageTextureName;
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
MinSize = (size, size);
|
||||
|
||||
AddChild(Button = new TextureRect
|
||||
{
|
||||
TextureScale = (2, 2),
|
||||
MouseFilter = MouseFilterMode.Stop
|
||||
});
|
||||
|
||||
AddChild(_highlightRect = new PanelContainer
|
||||
{
|
||||
StyleClasses = { StyleNano.StyleClassHandSlotHighlight },
|
||||
MinSize = (32, 32),
|
||||
Visible = false
|
||||
});
|
||||
|
||||
Button.OnKeyBindDown += OnButtonPressed;
|
||||
|
||||
AddChild(SpriteView = new SpriteView
|
||||
{
|
||||
Scale = (2, 2),
|
||||
OverrideDirection = Direction.South
|
||||
});
|
||||
|
||||
AddChild(HoverSpriteView = new SpriteView
|
||||
{
|
||||
Scale = (2, 2),
|
||||
OverrideDirection = Direction.South
|
||||
});
|
||||
|
||||
AddChild(StorageButton = new TextureButton
|
||||
{
|
||||
Scale = (0.75f, 0.75f),
|
||||
HorizontalAlignment = HAlignment.Right,
|
||||
VerticalAlignment = VAlignment.Bottom,
|
||||
Visible = false,
|
||||
});
|
||||
|
||||
StorageButton.OnKeyBindDown += args =>
|
||||
{
|
||||
if (args.Function != EngineKeyFunctions.UIClick)
|
||||
{
|
||||
OnButtonPressed(args);
|
||||
}
|
||||
};
|
||||
|
||||
StorageButton.OnPressed += OnStorageButtonPressed;
|
||||
|
||||
Button.OnMouseEntered += _ =>
|
||||
{
|
||||
MouseIsHovering = true;
|
||||
};
|
||||
Button.OnMouseEntered += OnButtonHover;
|
||||
|
||||
Button.OnMouseExited += _ =>
|
||||
{
|
||||
MouseIsHovering = false;
|
||||
ClearHover();
|
||||
};
|
||||
|
||||
AddChild(CooldownDisplay = new CooldownGraphic
|
||||
{
|
||||
Visible = false,
|
||||
});
|
||||
|
||||
RefreshTextures(gameHud);
|
||||
}
|
||||
|
||||
public void RefreshTextures(IGameHud gameHud)
|
||||
{
|
||||
Button.Texture = gameHud.GetHudTexture(_textureName);
|
||||
StorageButton.TextureNormal = gameHud.GetHudTexture(_storageTextureName);
|
||||
}
|
||||
|
||||
protected override void EnteredTree()
|
||||
{
|
||||
base.EnteredTree();
|
||||
|
||||
_itemSlotManager.EntityHighlightedUpdated += HandleEntitySlotHighlighted;
|
||||
UpdateSlotHighlighted();
|
||||
}
|
||||
|
||||
protected override void ExitedTree()
|
||||
{
|
||||
base.ExitedTree();
|
||||
|
||||
_itemSlotManager.EntityHighlightedUpdated -= HandleEntitySlotHighlighted;
|
||||
}
|
||||
|
||||
private void HandleEntitySlotHighlighted(EntitySlotHighlightedEventArgs entitySlotHighlightedEventArgs)
|
||||
{
|
||||
UpdateSlotHighlighted();
|
||||
}
|
||||
|
||||
public void UpdateSlotHighlighted()
|
||||
{
|
||||
Highlight(_itemSlotManager.IsHighlighted(Entity));
|
||||
}
|
||||
|
||||
public void ClearHover()
|
||||
{
|
||||
if (EntityHover)
|
||||
{
|
||||
ISpriteComponent? tempQualifier = HoverSpriteView.Sprite;
|
||||
if (tempQualifier != null)
|
||||
{
|
||||
IoCManager.Resolve<IEntityManager>().DeleteEntity(tempQualifier.Owner);
|
||||
}
|
||||
|
||||
HoverSpriteView.Sprite = null;
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void Highlight(bool highlight)
|
||||
{
|
||||
if (highlight)
|
||||
{
|
||||
_highlightRect.Visible = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_highlightRect.Visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnButtonPressed(GUIBoundKeyEventArgs args)
|
||||
{
|
||||
OnPressed?.Invoke(args);
|
||||
}
|
||||
|
||||
private void OnStorageButtonPressed(BaseButton.ButtonEventArgs args)
|
||||
{
|
||||
if (args.Event.Function == EngineKeyFunctions.UIClick)
|
||||
{
|
||||
OnStoragePressed?.Invoke(args.Event);
|
||||
}
|
||||
else
|
||||
{
|
||||
OnPressed?.Invoke(args.Event);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnButtonHover(GUIMouseHoverEventArgs args)
|
||||
{
|
||||
OnHover?.Invoke(args);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
using Content.Client.Resources;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using static Content.Client.IoC.StaticIoC;
|
||||
using static Robust.Client.UserInterface.Controls.BoxContainer;
|
||||
|
||||
namespace Content.Client.Items.UI
|
||||
{
|
||||
public sealed class ItemStatusPanel : Control
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
|
||||
[ViewVariables]
|
||||
private readonly Label _itemNameLabel;
|
||||
[ViewVariables]
|
||||
private readonly BoxContainer _statusContents;
|
||||
[ViewVariables]
|
||||
private readonly PanelContainer _panel;
|
||||
|
||||
[ViewVariables]
|
||||
private EntityUid? _entity;
|
||||
|
||||
public ItemStatusPanel(Texture texture, StyleBox.Margin cutout, StyleBox.Margin flat, Label.AlignMode textAlign)
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
var panel = new StyleBoxTexture
|
||||
{
|
||||
Texture = texture
|
||||
};
|
||||
panel.SetContentMarginOverride(StyleBox.Margin.Vertical, 4);
|
||||
panel.SetContentMarginOverride(StyleBox.Margin.Horizontal, 6);
|
||||
panel.SetPatchMargin(flat, 2);
|
||||
panel.SetPatchMargin(cutout, 13);
|
||||
|
||||
AddChild(_panel = new PanelContainer
|
||||
{
|
||||
PanelOverride = panel,
|
||||
ModulateSelfOverride = Color.White.WithAlpha(0.9f),
|
||||
Children =
|
||||
{
|
||||
new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Vertical,
|
||||
SeparationOverride = 0,
|
||||
Children =
|
||||
{
|
||||
(_statusContents = new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Vertical
|
||||
}),
|
||||
(_itemNameLabel = new Label
|
||||
{
|
||||
ClipText = true,
|
||||
StyleClasses = {StyleNano.StyleClassItemStatus},
|
||||
Align = textAlign
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
VerticalAlignment = VAlignment.Bottom;
|
||||
|
||||
// TODO: Depending on if its a two-hand panel or not
|
||||
MinSize = (150, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of <see cref="ItemStatusPanel"/>
|
||||
/// based on whether or not it is being created for the right
|
||||
/// or left hand.
|
||||
/// </summary>
|
||||
/// <param name="location">
|
||||
/// The location of the hand that this panel is for
|
||||
/// </param>
|
||||
/// <returns>the new <see cref="ItemStatusPanel"/> instance</returns>
|
||||
public static ItemStatusPanel FromSide(HandLocation location)
|
||||
{
|
||||
string texture;
|
||||
StyleBox.Margin cutOut;
|
||||
StyleBox.Margin flat;
|
||||
Label.AlignMode textAlign;
|
||||
|
||||
switch (location)
|
||||
{
|
||||
case HandLocation.Left:
|
||||
texture = "/Textures/Interface/Nano/item_status_right.svg.96dpi.png";
|
||||
cutOut = StyleBox.Margin.Left | StyleBox.Margin.Top;
|
||||
flat = StyleBox.Margin.Right | StyleBox.Margin.Bottom;
|
||||
textAlign = Label.AlignMode.Right;
|
||||
break;
|
||||
case HandLocation.Middle:
|
||||
texture = "/Textures/Interface/Nano/item_status_middle.svg.96dpi.png";
|
||||
cutOut = StyleBox.Margin.Right | StyleBox.Margin.Top;
|
||||
flat = StyleBox.Margin.Left | StyleBox.Margin.Bottom;
|
||||
textAlign = Label.AlignMode.Left;
|
||||
break;
|
||||
case HandLocation.Right:
|
||||
texture = "/Textures/Interface/Nano/item_status_left.svg.96dpi.png";
|
||||
cutOut = StyleBox.Margin.Right | StyleBox.Margin.Top;
|
||||
flat = StyleBox.Margin.Left | StyleBox.Margin.Bottom;
|
||||
textAlign = Label.AlignMode.Left;
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(location), location, null);
|
||||
}
|
||||
|
||||
return new ItemStatusPanel(ResC.GetTexture(texture), cutOut, flat, textAlign);
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
|
||||
UpdateItemName();
|
||||
}
|
||||
|
||||
public void Update(EntityUid? entity)
|
||||
{
|
||||
if (entity == null)
|
||||
{
|
||||
ClearOldStatus();
|
||||
_entity = null;
|
||||
_panel.Visible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (entity != _entity)
|
||||
{
|
||||
_entity = entity.Value;
|
||||
BuildNewEntityStatus();
|
||||
|
||||
UpdateItemName();
|
||||
}
|
||||
|
||||
_panel.Visible = true;
|
||||
}
|
||||
|
||||
private void UpdateItemName()
|
||||
{
|
||||
if (_entity == null)
|
||||
return;
|
||||
|
||||
if (_entityManager.TryGetComponent(_entity, out HandVirtualItemComponent? virtualItem)
|
||||
&& _entityManager.EntityExists(virtualItem.BlockingEntity))
|
||||
{
|
||||
// Uses identity because we can be blocked by pulling someone
|
||||
_itemNameLabel.Text = Identity.Name(virtualItem.BlockingEntity, _entityManager);
|
||||
}
|
||||
else
|
||||
{
|
||||
_itemNameLabel.Text = Identity.Name(_entity.Value, _entityManager);
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearOldStatus()
|
||||
{
|
||||
_statusContents.RemoveAllChildren();
|
||||
}
|
||||
|
||||
private void BuildNewEntityStatus()
|
||||
{
|
||||
DebugTools.AssertNotNull(_entity);
|
||||
|
||||
ClearOldStatus();
|
||||
|
||||
var collectMsg = new ItemStatusCollectMessage();
|
||||
_entityManager.EventBus.RaiseLocalEvent(_entity!.Value, collectMsg, true);
|
||||
|
||||
foreach (var control in collectMsg.Controls)
|
||||
{
|
||||
_statusContents.AddChild(control);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,10 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Content.Client.Chat;
|
||||
using Content.Client.Chat.Managers;
|
||||
using Content.Client.Options.UI;
|
||||
using Content.Client.GameTicking.Managers;
|
||||
using Content.Client.LateJoin;
|
||||
using Content.Client.Lobby.UI;
|
||||
using Content.Client.Preferences;
|
||||
using Content.Client.Preferences.UI;
|
||||
using Content.Client.Resources;
|
||||
using Content.Client.UserInterface.Systems.EscapeMenu;
|
||||
using Content.Client.Voting;
|
||||
using Content.Shared.GameTicking;
|
||||
using Robust.Client;
|
||||
using Robust.Client.Console;
|
||||
using Robust.Client.Input;
|
||||
@@ -20,13 +13,10 @@ using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
using Content.Client.UserInterface.Systems.EscapeMenu;
|
||||
|
||||
|
||||
namespace Content.Client.Lobby
|
||||
{
|
||||
@@ -74,14 +64,8 @@ namespace Content.Client.Lobby
|
||||
};
|
||||
|
||||
LayoutContainer.SetAnchorPreset(_lobby, LayoutContainer.LayoutPreset.Wide);
|
||||
|
||||
_chatManager.SetChatBox(_lobby.Chat);
|
||||
_voteManager.SetPopupContainer(_lobby.VoteContainer);
|
||||
|
||||
_lobby.ServerName.Text = _baseClient.GameInfo?.ServerName;
|
||||
|
||||
ChatInput.SetupChatInputHandlers(_inputManager, _lobby.Chat);
|
||||
|
||||
_lobby.ServerName.Text = _baseClient.GameInfo?.ServerName; //The eye of refactor gazes upon you...
|
||||
UpdateLobbyUi();
|
||||
|
||||
_lobby.CharacterPreview.CharacterSetupButton.OnPressed += _ =>
|
||||
@@ -107,7 +91,7 @@ namespace Content.Client.Lobby
|
||||
};
|
||||
|
||||
_lobby.LeaveButton.OnPressed += _ => _consoleHost.ExecuteCommand("disconnect");
|
||||
_lobby.OptionsButton.OnPressed += OnOptionsPressed;
|
||||
_lobby.OptionsButton.OnPressed += _ => _userInterfaceManager.GetUIController<OptionsUIController>().ToggleWindow();
|
||||
|
||||
|
||||
_gameTicker.InfoBlobUpdated += UpdateLobbyUi;
|
||||
@@ -115,11 +99,6 @@ namespace Content.Client.Lobby
|
||||
_gameTicker.LobbyLateJoinStatusUpdated += LobbyLateJoinStatusUpdated;
|
||||
}
|
||||
|
||||
private void OnOptionsPressed(BaseButton.ButtonEventArgs obj)
|
||||
{
|
||||
IoCManager.Resolve<IUserInterfaceManager>().GetUIController<OptionsUIController>().ToggleWindow();
|
||||
}
|
||||
|
||||
protected override void Shutdown()
|
||||
{
|
||||
_gameTicker.InfoBlobUpdated -= UpdateLobbyUi;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Linq;
|
||||
using Content.Client.HUD.UI;
|
||||
using Content.Client.Humanoid;
|
||||
using Content.Client.Inventory;
|
||||
using Content.Client.Preferences;
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
xmlns:vote="clr-namespace:Content.Client.Voting.UI"
|
||||
xmlns:style="clr-namespace:Content.Client.Stylesheets"
|
||||
xmlns:chatUi="clr-namespace:Content.Client.Chat.UI"
|
||||
xmlns:lobbyUi="clr-namespace:Content.Client.Lobby.UI"
|
||||
xmlns:info="clr-namespace:Content.Client.Info">
|
||||
xmlns:info="clr-namespace:Content.Client.Info"
|
||||
xmlns:widgets="clr-namespace:Content.Client.UserInterface.Systems.Chat.Widgets">
|
||||
<!-- Background -->
|
||||
<TextureRect Access="Public" Name = "Background" Stretch="KeepAspectCovered"/>
|
||||
<BoxContainer Orientation="Horizontal" Margin="10 10 10 10" SeparationOverride="2">
|
||||
@@ -42,7 +42,7 @@
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
<!-- Right Panel -->
|
||||
<PanelContainer Name="RightSide" StyleClasses="AngleRect" VerticalExpand="True" VerticalAlignment="Stretch">
|
||||
<PanelContainer Name="RightSide" StyleClasses="AngleRect" HorizontalAlignment= "Right" VerticalExpand="True" VerticalAlignment="Stretch">
|
||||
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
|
||||
<!-- Top row -->
|
||||
<BoxContainer Orientation="Horizontal" MinSize="0 40" Name="HeaderContainer" Access="Public" SeparationOverride="4">
|
||||
@@ -65,13 +65,13 @@
|
||||
<info:ServerInfo Name="ServerInfo" Access="Public" MinSize="0 30" VerticalExpand="false" Margin="3 3 3 3" MaxWidth="400" HorizontalAlignment="Left"/>
|
||||
<Label Name="StationTime" Access="Public" FontColorOverride="{x:Static maths:Color.LightGray}" Margin="3 3 3 3" HorizontalAlignment="Left"/>
|
||||
<controls:HSpacer Spacing="5"/>
|
||||
<lobbyUi:LobbyCharacterPreviewPanel Name="CharacterPreview" Access="Public"></lobbyUi:LobbyCharacterPreviewPanel>
|
||||
<lobbyUi:LobbyCharacterPreviewPanel Name="CharacterPreview" Access="Public" />
|
||||
<controls:HSpacer Spacing="5"/>
|
||||
<BoxContainer MinHeight="10"/>
|
||||
<!-- Gold line -->
|
||||
<controls:HLine Color="{x:Static style:StyleNano.NanoGold}" Thickness="2" Access="Public"/>
|
||||
<controls:HSpacer Spacing="10"/>
|
||||
<chatUi:ChatBox Name="Chat" Access="Public" VerticalExpand="True" Margin="3 3 3 3" MinHeight="50"/>
|
||||
<widgets:ChatBox Name="Chat" Access="Public" VerticalExpand="True" Margin="3 3 3 3" MinHeight="50" Main="True"/>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</SplitContainer>
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Client.Options.UI;
|
||||
using Content.Client.MainMenu.UI;
|
||||
using Content.Client.UserInterface.Systems.EscapeMenu;
|
||||
using Robust.Client;
|
||||
@@ -44,6 +42,7 @@ namespace Content.Client.MainMenu
|
||||
_mainMenuControl.OptionsButton.OnPressed += OptionsButtonPressed;
|
||||
_mainMenuControl.DirectConnectButton.OnPressed += DirectConnectButtonPressed;
|
||||
_mainMenuControl.AddressBox.OnTextEntered += AddressBoxEntered;
|
||||
_mainMenuControl.ChangelogButton.OnPressed += ChangelogButtonPressed;
|
||||
|
||||
_client.RunLevelChanged += RunLevelChanged;
|
||||
}
|
||||
@@ -57,14 +56,19 @@ namespace Content.Client.MainMenu
|
||||
_mainMenuControl.Dispose();
|
||||
}
|
||||
|
||||
private void QuitButtonPressed(BaseButton.ButtonEventArgs args)
|
||||
private void ChangelogButtonPressed(BaseButton.ButtonEventArgs args)
|
||||
{
|
||||
_controllerProxy.Shutdown();
|
||||
_userInterfaceManager.GetUIController<ChangelogUIController>().ToggleWindow();
|
||||
}
|
||||
|
||||
private void OptionsButtonPressed(BaseButton.ButtonEventArgs args)
|
||||
{
|
||||
IoCManager.Resolve<IUserInterfaceManager>().GetUIController<OptionsUIController>().ToggleWindow();
|
||||
_userInterfaceManager.GetUIController<OptionsUIController>().ToggleWindow();
|
||||
}
|
||||
|
||||
private void QuitButtonPressed(BaseButton.ButtonEventArgs args)
|
||||
{
|
||||
_controllerProxy.Shutdown();
|
||||
}
|
||||
|
||||
private void DirectConnectButtonPressed(BaseButton.ButtonEventArgs args)
|
||||
|
||||
@@ -137,7 +137,7 @@ namespace Content.Client.Options.UI.Tabs
|
||||
AddButton(ContentKeyFunctions.OpenCharacterMenu);
|
||||
AddButton(ContentKeyFunctions.OpenCraftingMenu);
|
||||
AddButton(ContentKeyFunctions.OpenInventoryMenu);
|
||||
AddButton(ContentKeyFunctions.OpenInfo);
|
||||
AddButton(ContentKeyFunctions.OpenAHelp);
|
||||
AddButton(ContentKeyFunctions.OpenActionsMenu);
|
||||
AddButton(ContentKeyFunctions.OpenEntitySpawnWindow);
|
||||
AddButton(ContentKeyFunctions.OpenSandboxWindow);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Linq;
|
||||
using Content.Client.HUD.UI;
|
||||
using Content.Client.Humanoid;
|
||||
using Content.Client.Lobby.UI;
|
||||
using Content.Client.Message;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Content.Client.Administration.Managers;
|
||||
using Content.Client.HUD;
|
||||
using Content.Shared.Sandbox;
|
||||
using Robust.Client.Console;
|
||||
using Robust.Client.Placement;
|
||||
@@ -17,7 +16,6 @@ namespace Content.Client.Sandbox
|
||||
[Dependency] private readonly IMapManager _map = default!;
|
||||
[Dependency] private readonly IPlacementManager _placement = default!;
|
||||
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
|
||||
[Dependency] private readonly IGameHud _gameHud = default!;
|
||||
|
||||
private bool _sandboxEnabled;
|
||||
public bool SandboxAllowed { get; private set; }
|
||||
@@ -39,12 +37,10 @@ namespace Content.Client.Sandbox
|
||||
if (SandboxAllowed)
|
||||
{
|
||||
SandboxEnabled?.Invoke();
|
||||
_gameHud.SandboxButtonVisible = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
SandboxDisabled?.Invoke();
|
||||
_gameHud.SandboxButtonVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
using Content.Client.Storage.UI;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Input;
|
||||
using Content.Client.Items.Managers;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using JetBrains.Annotations;
|
||||
using static Content.Shared.Storage.SharedStorageComponent;
|
||||
|
||||
namespace Content.Client.Storage
|
||||
@@ -49,10 +47,6 @@ namespace Content.Client.Storage
|
||||
{
|
||||
SendMessage(new StorageInteractWithItemEvent(entity));
|
||||
}
|
||||
else if (IoCManager.Resolve<IEntityManager>().EntityExists(entity))
|
||||
{
|
||||
IoCManager.Resolve<IItemSlotManager>().OnButtonPressed(args.Event, entity);
|
||||
}
|
||||
}
|
||||
|
||||
public void TouchedContainerButton(BaseButton.ButtonEventArgs args)
|
||||
|
||||
@@ -2,9 +2,9 @@ using System.Linq;
|
||||
using Content.Client.Actions.UI;
|
||||
using Content.Client.ContextMenu.UI;
|
||||
using Content.Client.Examine;
|
||||
using Content.Client.HUD.UI;
|
||||
using Content.Client.Resources;
|
||||
using Content.Client.Targeting;
|
||||
using Content.Client.Targeting.UI;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Client.Verbs.UI;
|
||||
using Content.Shared.Verbs;
|
||||
@@ -39,7 +39,7 @@ namespace Content.Client.Stylesheets
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// STLYE SHEETS WERE A MISTAKE. KILL ALL OF THIS WITH FIRE
|
||||
public sealed class StyleNano : StyleBase
|
||||
{
|
||||
public const string StyleClassBorderedWindowPanel = "BorderedWindowPanel";
|
||||
@@ -741,7 +741,6 @@ namespace Content.Client.Stylesheets
|
||||
Element<ContainerButton>().Class(StyleClassStorageButton)
|
||||
.Pseudo(ContainerButton.StylePseudoClassDisabled)
|
||||
.Prop(Control.StylePropertyModulateSelf, ButtonColorDisabled),
|
||||
|
||||
// ListContainer
|
||||
Element<ContainerButton>().Class(ListContainer.StyleClassListContainerButton)
|
||||
.Prop(ContainerButton.StylePropertyStyleBox, listContainerButton),
|
||||
@@ -762,43 +761,6 @@ namespace Content.Client.Stylesheets
|
||||
.Pseudo(ContainerButton.StylePseudoClassDisabled)
|
||||
.Prop(Control.StylePropertyModulateSelf, new Color(10, 10, 12)),
|
||||
|
||||
// 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),
|
||||
@@ -1131,70 +1093,70 @@ namespace Content.Client.Stylesheets
|
||||
// which is NOT the case for the default BaseButton styles (OpenLeft/OpenRight adds extra padding on one of the sides
|
||||
// which makes the TopButton icons appear off-center, which we don't want).
|
||||
new StyleRule(
|
||||
new SelectorElement(typeof(TopButton), new[] {ButtonSquare}, null, null),
|
||||
new SelectorElement(typeof(MenuButton), new[] {ButtonSquare}, null, null),
|
||||
new[]
|
||||
{
|
||||
new StyleProperty(Button.StylePropertyStyleBox, topButtonSquare),
|
||||
}),
|
||||
|
||||
new StyleRule(
|
||||
new SelectorElement(typeof(TopButton), new[] {ButtonOpenLeft}, null, null),
|
||||
new SelectorElement(typeof(MenuButton), new[] {ButtonOpenLeft}, null, null),
|
||||
new[]
|
||||
{
|
||||
new StyleProperty(Button.StylePropertyStyleBox, topButtonOpenLeft),
|
||||
}),
|
||||
|
||||
new StyleRule(
|
||||
new SelectorElement(typeof(TopButton), new[] {ButtonOpenRight}, null, null),
|
||||
new SelectorElement(typeof(MenuButton), new[] {ButtonOpenRight}, null, null),
|
||||
new[]
|
||||
{
|
||||
new StyleProperty(Button.StylePropertyStyleBox, topButtonOpenRight),
|
||||
}),
|
||||
|
||||
new StyleRule(
|
||||
new SelectorElement(typeof(TopButton), null, null, new[] {Button.StylePseudoClassNormal}),
|
||||
new SelectorElement(typeof(MenuButton), null, null, new[] {Button.StylePseudoClassNormal}),
|
||||
new[]
|
||||
{
|
||||
new StyleProperty(Button.StylePropertyModulateSelf, ButtonColorDefault),
|
||||
}),
|
||||
|
||||
new StyleRule(
|
||||
new SelectorElement(typeof(TopButton), new[] {TopButton.StyleClassRedTopButton}, null, new[] {Button.StylePseudoClassNormal}),
|
||||
new SelectorElement(typeof(MenuButton), new[] {MenuButton.StyleClassRedTopButton}, null, new[] {Button.StylePseudoClassNormal}),
|
||||
new[]
|
||||
{
|
||||
new StyleProperty(Button.StylePropertyModulateSelf, ButtonColorDefaultRed),
|
||||
}),
|
||||
|
||||
new StyleRule(
|
||||
new SelectorElement(typeof(TopButton), null, null, new[] {Button.StylePseudoClassNormal}),
|
||||
new SelectorElement(typeof(MenuButton), null, null, new[] {Button.StylePseudoClassNormal}),
|
||||
new[]
|
||||
{
|
||||
new StyleProperty(Button.StylePropertyModulateSelf, ButtonColorDefault),
|
||||
}),
|
||||
|
||||
new StyleRule(
|
||||
new SelectorElement(typeof(TopButton), null, null, new[] {Button.StylePseudoClassPressed}),
|
||||
new SelectorElement(typeof(MenuButton), null, null, new[] {Button.StylePseudoClassPressed}),
|
||||
new[]
|
||||
{
|
||||
new StyleProperty(Button.StylePropertyModulateSelf, ButtonColorPressed),
|
||||
}),
|
||||
|
||||
new StyleRule(
|
||||
new SelectorElement(typeof(TopButton), null, null, new[] {Button.StylePseudoClassHover}),
|
||||
new SelectorElement(typeof(MenuButton), null, null, new[] {Button.StylePseudoClassHover}),
|
||||
new[]
|
||||
{
|
||||
new StyleProperty(Button.StylePropertyModulateSelf, ButtonColorHovered),
|
||||
}),
|
||||
|
||||
new StyleRule(
|
||||
new SelectorElement(typeof(TopButton), new[] {TopButton.StyleClassRedTopButton}, null, new[] {Button.StylePseudoClassHover}),
|
||||
new SelectorElement(typeof(MenuButton), new[] {MenuButton.StyleClassRedTopButton}, null, new[] {Button.StylePseudoClassHover}),
|
||||
new[]
|
||||
{
|
||||
new StyleProperty(Button.StylePropertyModulateSelf, ButtonColorHoveredRed),
|
||||
}),
|
||||
|
||||
new StyleRule(
|
||||
new SelectorElement(typeof(Label), new[] {TopButton.StyleClassLabelTopButton}, null, null),
|
||||
new SelectorElement(typeof(Label), new[] {MenuButton.StyleClassLabelTopButton}, null, null),
|
||||
new[]
|
||||
{
|
||||
new StyleProperty(Label.StylePropertyFont, notoSansDisplayBold14),
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
using System.Collections.Generic;
|
||||
using Content.Client.HUD;
|
||||
using Content.Shared.Suspicion;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.ViewVariables;
|
||||
using Robust.Client.UserInterface;
|
||||
using static Robust.Client.UserInterface.Controls.LayoutContainer;
|
||||
|
||||
namespace Content.Client.Suspicion
|
||||
{
|
||||
[RegisterComponent]
|
||||
public sealed class SuspicionRoleComponent : SharedSuspicionRoleComponent
|
||||
{
|
||||
[Dependency] private readonly IGameHud _gameHud = default!;
|
||||
[Dependency] private readonly IOverlayManager _overlayManager = default!;
|
||||
[Dependency] private readonly IResourceCache _resourceCache = default!;
|
||||
[Dependency] private readonly IUserInterfaceManager _ui = default!;
|
||||
|
||||
private SuspicionGui? _gui;
|
||||
private string? _role;
|
||||
@@ -101,25 +97,18 @@ namespace Content.Client.Suspicion
|
||||
Allies.AddRange(state.Allies);
|
||||
}
|
||||
|
||||
public void PlayerDetached()
|
||||
public void RemoveUI()
|
||||
{
|
||||
_gui?.Parent?.RemoveChild(_gui);
|
||||
RemoveTraitorOverlay();
|
||||
}
|
||||
|
||||
public void PlayerAttached()
|
||||
public void AddUI()
|
||||
{
|
||||
if (_gui == null)
|
||||
{
|
||||
_gui = new SuspicionGui();
|
||||
}
|
||||
else
|
||||
{
|
||||
_gui.Parent?.RemoveChild(_gui);
|
||||
}
|
||||
|
||||
_gameHud.SuspicionContainer.AddChild(_gui);
|
||||
_gui.UpdateLabel();
|
||||
// TODO move this out of the component
|
||||
_gui = _ui.ActiveScreen?.GetOrNewWidget<SuspicionGui>();
|
||||
_gui!.UpdateLabel();
|
||||
SetAnchorAndMarginPreset(_gui, LayoutPreset.BottomLeft);
|
||||
|
||||
if (_antagonist ?? false)
|
||||
{
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Client.Suspicion
|
||||
{
|
||||
@@ -9,8 +8,11 @@ namespace Content.Client.Suspicion
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<SuspicionRoleComponent, PlayerAttachedEvent>((_, component, _) => component.PlayerAttached());
|
||||
SubscribeLocalEvent<SuspicionRoleComponent, PlayerDetachedEvent>((_, component, _) => component.PlayerDetached());
|
||||
SubscribeLocalEvent<SuspicionRoleComponent, ComponentAdd>((_, component, _) => component.AddUI());
|
||||
SubscribeLocalEvent<SuspicionRoleComponent, ComponentRemove>((_, component, _) => component.RemoveUI());
|
||||
|
||||
SubscribeLocalEvent<SuspicionRoleComponent, PlayerAttachedEvent>((_, component, _) => component.AddUI());
|
||||
SubscribeLocalEvent<SuspicionRoleComponent, PlayerDetachedEvent>((_, component, _) => component.RemoveUI());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
using System;
|
||||
using Content.Client.Resources;
|
||||
using Content.Shared.Targeting;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
|
||||
namespace Content.Client.Targeting
|
||||
{
|
||||
public sealed class TargetingDoll : BoxContainer
|
||||
{
|
||||
private TargetingZone _activeZone = TargetingZone.Middle;
|
||||
public const string StyleClassTargetDollZone = "target-doll-zone";
|
||||
|
||||
private const string TextureHigh = "/Textures/Interface/target-doll-high.svg.96dpi.png";
|
||||
private const string TextureMiddle = "/Textures/Interface/target-doll-middle.svg.96dpi.png";
|
||||
private const string TextureLow = "/Textures/Interface/target-doll-low.svg.96dpi.png";
|
||||
|
||||
private readonly TextureButton _buttonHigh;
|
||||
private readonly TextureButton _buttonMiddle;
|
||||
private readonly TextureButton _buttonLow;
|
||||
|
||||
public TargetingZone ActiveZone
|
||||
{
|
||||
get => _activeZone;
|
||||
set
|
||||
{
|
||||
if (_activeZone == value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_activeZone = value;
|
||||
OnZoneChanged?.Invoke(value);
|
||||
|
||||
UpdateButtons();
|
||||
}
|
||||
}
|
||||
|
||||
public event Action<TargetingZone>? OnZoneChanged;
|
||||
|
||||
public TargetingDoll(IResourceCache resourceCache)
|
||||
{
|
||||
Orientation = LayoutOrientation.Vertical;
|
||||
|
||||
_buttonHigh = new TextureButton
|
||||
{
|
||||
TextureNormal = resourceCache.GetTexture(TextureHigh),
|
||||
StyleClasses = {StyleClassTargetDollZone},
|
||||
HorizontalAlignment = HAlignment.Center
|
||||
};
|
||||
|
||||
_buttonMiddle = new TextureButton
|
||||
{
|
||||
TextureNormal = resourceCache.GetTexture(TextureMiddle),
|
||||
StyleClasses = {StyleClassTargetDollZone},
|
||||
HorizontalAlignment = HAlignment.Center
|
||||
};
|
||||
|
||||
_buttonLow = new TextureButton
|
||||
{
|
||||
TextureNormal = resourceCache.GetTexture(TextureLow),
|
||||
StyleClasses = {StyleClassTargetDollZone},
|
||||
HorizontalAlignment = HAlignment.Center
|
||||
};
|
||||
|
||||
_buttonHigh.OnPressed += _ => ActiveZone = TargetingZone.High;
|
||||
_buttonMiddle.OnPressed += _ => ActiveZone = TargetingZone.Middle;
|
||||
_buttonLow.OnPressed += _ => ActiveZone = TargetingZone.Low;
|
||||
|
||||
AddChild(_buttonHigh);
|
||||
AddChild(_buttonMiddle);
|
||||
AddChild(_buttonLow);
|
||||
|
||||
UpdateButtons();
|
||||
}
|
||||
|
||||
private void UpdateButtons()
|
||||
{
|
||||
_buttonHigh.Pressed = _activeZone == TargetingZone.High;
|
||||
_buttonMiddle.Pressed = _activeZone == TargetingZone.Middle;
|
||||
_buttonLow.Pressed = _activeZone == TargetingZone.Low;
|
||||
}
|
||||
}
|
||||
}
|
||||
18
Content.Client/Targeting/UI/TargetingDoll.xaml
Normal file
18
Content.Client/Targeting/UI/TargetingDoll.xaml
Normal file
@@ -0,0 +1,18 @@
|
||||
<targeting:TargetingDoll xmlns="https://spacestation14.io"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:maths="clr-namespace:Robust.Shared.Maths;assembly=Robust.Shared.Maths"
|
||||
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
|
||||
xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
xmlns:parallax="clr-namespace:Content.Client.Parallax"
|
||||
xmlns:vote="clr-namespace:Content.Client.Voting.UI"
|
||||
xmlns:style="clr-namespace:Content.Client.Stylesheets"
|
||||
xmlns:chatUi="clr-namespace:Content.Client.Chat.UI"
|
||||
xmlns:lobbyUi="clr-namespace:Content.Client.Lobby.UI"
|
||||
xmlns:info="clr-namespace:Content.Client.Info"
|
||||
xmlns:targeting="clr-namespace:Content.Client.Targeting.UI"
|
||||
Orientation="Vertical"
|
||||
>
|
||||
<TextureButton Name = "ButtonHigh" TexturePath="/Textures/Interface/target-doll-high.svg.96dpi.png" HorizontalAlignment="Center" StyleIdentifier="target-doll-zone"/>
|
||||
<TextureButton Name = "ButtonMedium" TexturePath="/Textures/Interface/target-doll-middle.svg.96dpi.png" HorizontalAlignment="Center" StyleIdentifier="target-doll-zone"/>
|
||||
<TextureButton Name = "ButtonLow" TexturePath="/Textures/Interface/target-doll-low.svg.96dpi.png" HorizontalAlignment="Center" StyleIdentifier="target-doll-zone"/>
|
||||
</targeting:TargetingDoll>
|
||||
44
Content.Client/Targeting/UI/TargetingDoll.xaml.cs
Normal file
44
Content.Client/Targeting/UI/TargetingDoll.xaml.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using Content.Shared.Targeting;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.Targeting.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class TargetingDoll : BoxContainer
|
||||
{
|
||||
public static readonly string StyleClassTargetDollZone = "target-doll-zone";
|
||||
|
||||
|
||||
private TargetingZone _activeZone = TargetingZone.Middle;
|
||||
|
||||
public event Action<TargetingZone>? OnZoneChanged;
|
||||
public TargetingDoll()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public TargetingZone ActiveZone
|
||||
{
|
||||
get => _activeZone;
|
||||
set
|
||||
{
|
||||
if (_activeZone == value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_activeZone = value;
|
||||
OnZoneChanged?.Invoke(value);
|
||||
|
||||
UpdateButtons();
|
||||
}
|
||||
}
|
||||
private void UpdateButtons()
|
||||
{
|
||||
ButtonHigh.Pressed = _activeZone == TargetingZone.High;
|
||||
ButtonMedium.Pressed = _activeZone == TargetingZone.Middle;
|
||||
ButtonLow.Pressed = _activeZone == TargetingZone.Low;
|
||||
}
|
||||
}
|
||||
106
Content.Client/UserInterface/BoundKeyHelpers.cs
Normal file
106
Content.Client/UserInterface/BoundKeyHelpers.cs
Normal file
@@ -0,0 +1,106 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.UserInterface;
|
||||
|
||||
public static class BoundKeyHelper
|
||||
{
|
||||
public static string ShortKeyName(BoundKeyFunction keyFunction)
|
||||
{
|
||||
// need to use shortened key names so they fit in the buttons.
|
||||
return TryGetShortKeyName(keyFunction, out var name) ? Loc.GetString(name) : " ";
|
||||
}
|
||||
|
||||
private static string? DefaultShortKeyName(BoundKeyFunction keyFunction)
|
||||
{
|
||||
var name = FormattedMessage.EscapeText(IoCManager.Resolve<IInputManager>().GetKeyFunctionButtonString(keyFunction));
|
||||
return name.Length > 3 ? null : name;
|
||||
}
|
||||
|
||||
public static bool TryGetShortKeyName(BoundKeyFunction keyFunction, [NotNullWhen(true)] out string? name)
|
||||
{
|
||||
if (IoCManager.Resolve<IInputManager>().TryGetKeyBinding(keyFunction, out var binding))
|
||||
{
|
||||
// can't possibly fit a modifier key in the top button, so omit it
|
||||
var key = binding.BaseKey;
|
||||
if (binding.Mod1 != Keyboard.Key.Unknown || binding.Mod2 != Keyboard.Key.Unknown ||
|
||||
binding.Mod3 != Keyboard.Key.Unknown)
|
||||
{
|
||||
name = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
name = null;
|
||||
name = key switch
|
||||
{
|
||||
Keyboard.Key.Apostrophe => "'",
|
||||
Keyboard.Key.Comma => ",",
|
||||
Keyboard.Key.Delete => "Del",
|
||||
Keyboard.Key.Down => "Dwn",
|
||||
Keyboard.Key.Escape => "Esc",
|
||||
Keyboard.Key.Equal => "=",
|
||||
Keyboard.Key.Home => "Hom",
|
||||
Keyboard.Key.Insert => "Ins",
|
||||
Keyboard.Key.Left => "Lft",
|
||||
Keyboard.Key.Menu => "Men",
|
||||
Keyboard.Key.Minus => "-",
|
||||
Keyboard.Key.Num0 => "0",
|
||||
Keyboard.Key.Num1 => "1",
|
||||
Keyboard.Key.Num2 => "2",
|
||||
Keyboard.Key.Num3 => "3",
|
||||
Keyboard.Key.Num4 => "4",
|
||||
Keyboard.Key.Num5 => "5",
|
||||
Keyboard.Key.Num6 => "6",
|
||||
Keyboard.Key.Num7 => "7",
|
||||
Keyboard.Key.Num8 => "8",
|
||||
Keyboard.Key.Num9 => "9",
|
||||
Keyboard.Key.Pause => "||",
|
||||
Keyboard.Key.Period => ".",
|
||||
Keyboard.Key.Return => "Ret",
|
||||
Keyboard.Key.Right => "Rgt",
|
||||
Keyboard.Key.Slash => "/",
|
||||
Keyboard.Key.Space => "Spc",
|
||||
Keyboard.Key.Tab => "Tab",
|
||||
Keyboard.Key.Tilde => "~",
|
||||
Keyboard.Key.BackSlash => "\\",
|
||||
Keyboard.Key.BackSpace => "Bks",
|
||||
Keyboard.Key.LBracket => "[",
|
||||
Keyboard.Key.MouseButton4 => "M4",
|
||||
Keyboard.Key.MouseButton5 => "M5",
|
||||
Keyboard.Key.MouseButton6 => "M6",
|
||||
Keyboard.Key.MouseButton7 => "M7",
|
||||
Keyboard.Key.MouseButton8 => "M8",
|
||||
Keyboard.Key.MouseButton9 => "M9",
|
||||
Keyboard.Key.MouseLeft => "ML",
|
||||
Keyboard.Key.MouseMiddle => "MM",
|
||||
Keyboard.Key.MouseRight => "MR",
|
||||
Keyboard.Key.NumpadDecimal => "N.",
|
||||
Keyboard.Key.NumpadDivide => "N/",
|
||||
Keyboard.Key.NumpadEnter => "Ent",
|
||||
Keyboard.Key.NumpadMultiply => "*",
|
||||
Keyboard.Key.NumpadNum0 => "0",
|
||||
Keyboard.Key.NumpadNum1 => "1",
|
||||
Keyboard.Key.NumpadNum2 => "2",
|
||||
Keyboard.Key.NumpadNum3 => "3",
|
||||
Keyboard.Key.NumpadNum4 => "4",
|
||||
Keyboard.Key.NumpadNum5 => "5",
|
||||
Keyboard.Key.NumpadNum6 => "6",
|
||||
Keyboard.Key.NumpadNum7 => "7",
|
||||
Keyboard.Key.NumpadNum8 => "8",
|
||||
Keyboard.Key.NumpadNum9 => "9",
|
||||
Keyboard.Key.NumpadSubtract => "N-",
|
||||
Keyboard.Key.PageDown => "PgD",
|
||||
Keyboard.Key.PageUp => "PgU",
|
||||
Keyboard.Key.RBracket => "]",
|
||||
Keyboard.Key.SemiColon => ";",
|
||||
_ => DefaultShortKeyName(keyFunction)
|
||||
};
|
||||
return name != null;
|
||||
}
|
||||
|
||||
name = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
144
Content.Client/UserInterface/Controls/MenuButton.cs
Normal file
144
Content.Client/UserInterface/Controls/MenuButton.cs
Normal file
@@ -0,0 +1,144 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.UserInterface.Controls;
|
||||
|
||||
public sealed class MenuButton : ContainerButton
|
||||
{
|
||||
[Dependency] private readonly IInputManager _inputManager = default!;
|
||||
public const string StyleClassLabelTopButton = "topButtonLabel";
|
||||
public const string StyleClassRedTopButton = "topButtonLabel";
|
||||
private const float CustomTooltipDelay = 0.4f;
|
||||
|
||||
private static readonly Color ColorNormal = Color.FromHex("#7b7e9e");
|
||||
private static readonly Color ColorRedNormal = Color.FromHex("#FEFEFE");
|
||||
private static readonly Color ColorHovered = Color.FromHex("#9699bb");
|
||||
private static readonly Color ColorRedHovered = Color.FromHex("#FFFFFF");
|
||||
private static readonly Color ColorPressed = Color.FromHex("#789B8C");
|
||||
|
||||
private const float VertPad = 8f;
|
||||
private Color NormalColor => HasStyleClass(StyleClassRedTopButton) ? ColorRedNormal : ColorNormal;
|
||||
private Color HoveredColor => HasStyleClass(StyleClassRedTopButton) ? ColorRedHovered : ColorHovered;
|
||||
|
||||
private BoundKeyFunction _function;
|
||||
private readonly BoxContainer _root;
|
||||
private readonly TextureRect _buttonIcon;
|
||||
private readonly Label _buttonLabel;
|
||||
|
||||
public string AppendStyleClass { set => AddStyleClass(value); }
|
||||
public Texture? Icon { get => _buttonIcon.Texture; set => _buttonIcon.Texture = value; }
|
||||
|
||||
public BoundKeyFunction BoundKey
|
||||
{
|
||||
get => _function;
|
||||
set
|
||||
{
|
||||
_function = value;
|
||||
_buttonLabel.Text = BoundKeyHelper.ShortKeyName(value);
|
||||
}
|
||||
}
|
||||
|
||||
public BoxContainer ButtonRoot => _root;
|
||||
|
||||
public MenuButton()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
TooltipDelay = CustomTooltipDelay;
|
||||
_buttonIcon = new TextureRect()
|
||||
{
|
||||
TextureScale = (0.5f, 0.5f),
|
||||
HorizontalAlignment = HAlignment.Center,
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
VerticalExpand = true,
|
||||
Margin = new Thickness(0, VertPad),
|
||||
ModulateSelfOverride = NormalColor,
|
||||
Stretch = TextureRect.StretchMode.KeepCentered
|
||||
};
|
||||
_buttonLabel = new Label
|
||||
{
|
||||
Text = "",
|
||||
HorizontalAlignment = HAlignment.Center,
|
||||
ModulateSelfOverride = NormalColor,
|
||||
StyleClasses = {StyleClassLabelTopButton}
|
||||
};
|
||||
_root = new BoxContainer
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Vertical,
|
||||
Children =
|
||||
{
|
||||
_buttonIcon,
|
||||
_buttonLabel
|
||||
}
|
||||
};
|
||||
AddChild(_root);
|
||||
ToggleMode = true;
|
||||
}
|
||||
|
||||
protected override void EnteredTree()
|
||||
{
|
||||
_inputManager.OnKeyBindingAdded += OnKeyBindingChanged;
|
||||
_inputManager.OnKeyBindingRemoved += OnKeyBindingChanged;
|
||||
_inputManager.OnInputModeChanged += OnKeyBindingChanged;
|
||||
}
|
||||
|
||||
protected override void ExitedTree()
|
||||
{
|
||||
_inputManager.OnKeyBindingAdded -= OnKeyBindingChanged;
|
||||
_inputManager.OnKeyBindingRemoved -= OnKeyBindingChanged;
|
||||
_inputManager.OnInputModeChanged -= OnKeyBindingChanged;
|
||||
}
|
||||
|
||||
|
||||
private void OnKeyBindingChanged(IKeyBinding obj)
|
||||
{
|
||||
_buttonLabel.Text = BoundKeyHelper.ShortKeyName(_function);
|
||||
}
|
||||
|
||||
private void OnKeyBindingChanged()
|
||||
{
|
||||
_buttonLabel.Text = BoundKeyHelper.ShortKeyName(_function);
|
||||
}
|
||||
|
||||
protected override void StylePropertiesChanged()
|
||||
{
|
||||
// colors of children depend on style, so ensure we update when style is changed
|
||||
base.StylePropertiesChanged();
|
||||
UpdateChildColors();
|
||||
}
|
||||
|
||||
private void UpdateChildColors()
|
||||
{
|
||||
if (_buttonIcon == null || _buttonLabel == null) return;
|
||||
switch (DrawMode)
|
||||
{
|
||||
case DrawModeEnum.Normal:
|
||||
_buttonIcon.ModulateSelfOverride = NormalColor;
|
||||
_buttonLabel.ModulateSelfOverride = NormalColor;
|
||||
break;
|
||||
|
||||
case DrawModeEnum.Pressed:
|
||||
_buttonIcon.ModulateSelfOverride = ColorPressed;
|
||||
_buttonLabel.ModulateSelfOverride = ColorPressed;
|
||||
break;
|
||||
|
||||
case DrawModeEnum.Hover:
|
||||
_buttonIcon.ModulateSelfOverride = HoveredColor;
|
||||
_buttonLabel.ModulateSelfOverride = HoveredColor;
|
||||
break;
|
||||
|
||||
case DrawModeEnum.Disabled:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected override void DrawModeChanged()
|
||||
{
|
||||
base.DrawModeChanged();
|
||||
UpdateChildColors();
|
||||
}
|
||||
}
|
||||
18
Content.Client/UserInterface/Controls/SlotButton.cs
Normal file
18
Content.Client/UserInterface/Controls/SlotButton.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Content.Client.Inventory;
|
||||
|
||||
namespace Content.Client.UserInterface.Controls
|
||||
{
|
||||
public sealed class SlotButton : SlotControl
|
||||
{
|
||||
public SlotButton(){}
|
||||
|
||||
public SlotButton(ClientInventorySystem.SlotData slotData)
|
||||
{
|
||||
ButtonTexturePath = slotData.TextureName;
|
||||
Blocked = slotData.Blocked;
|
||||
Highlight = slotData.Highlighted;
|
||||
StorageTexturePath = "Slots/back";
|
||||
SlotName = slotData.SlotName;
|
||||
}
|
||||
}
|
||||
}
|
||||
236
Content.Client/UserInterface/Controls/SlotControl.cs
Normal file
236
Content.Client/UserInterface/Controls/SlotControl.cs
Normal file
@@ -0,0 +1,236 @@
|
||||
using Content.Client.Cooldown;
|
||||
using Content.Client.UserInterface.Systems.Inventory.Controls;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Input;
|
||||
|
||||
namespace Content.Client.UserInterface.Controls
|
||||
{
|
||||
[Virtual]
|
||||
public abstract class SlotControl : Control
|
||||
{
|
||||
private const string HighlightShader = "SelectionOutlineInrange";
|
||||
|
||||
public TextureRect ButtonRect { get; }
|
||||
public TextureRect BlockedRect { get; }
|
||||
public TextureRect HighlightRect { get; }
|
||||
public SpriteView SpriteView { get; }
|
||||
public SpriteView HoverSpriteView { get; }
|
||||
public TextureButton StorageButton { get; }
|
||||
public CooldownGraphic CooldownDisplay { get; }
|
||||
|
||||
public EntityUid? Entity => SpriteView.Sprite?.Owner;
|
||||
|
||||
private bool _slotNameSet;
|
||||
|
||||
private string _slotName = "";
|
||||
public string SlotName
|
||||
{
|
||||
get => _slotName;
|
||||
set
|
||||
{
|
||||
//this auto registers the button with it's parent container when it's set
|
||||
if (_slotNameSet)
|
||||
{
|
||||
Logger.Warning("Tried to set slotName after init for:" + Name);
|
||||
return;
|
||||
}
|
||||
_slotNameSet = true;
|
||||
if (Parent is IItemslotUIContainer container)
|
||||
{
|
||||
container.TryRegisterButton(this, value);
|
||||
}
|
||||
Name = "SlotButton_" + value;
|
||||
_slotName = value;
|
||||
}
|
||||
}
|
||||
|
||||
public bool Highlight { get => HighlightRect.Visible; set => HighlightRect.Visible = value;}
|
||||
|
||||
public bool Blocked { get => BlockedRect.Visible; set => BlockedRect.Visible = value;}
|
||||
|
||||
public Texture BlockedTexture => Theme.ResolveTexture(BlockedTexturePath);
|
||||
|
||||
private string _blockedTexturePath = "";
|
||||
public string BlockedTexturePath
|
||||
{
|
||||
get => _blockedTexturePath;
|
||||
set
|
||||
{
|
||||
_blockedTexturePath = value;
|
||||
BlockedRect.Texture = Theme.ResolveTexture(_blockedTexturePath);
|
||||
}
|
||||
}
|
||||
|
||||
public Texture ButtonTexture => Theme.ResolveTexture(ButtonTexturePath);
|
||||
|
||||
private string _buttonTexturePath = "";
|
||||
public string ButtonTexturePath {
|
||||
get => _buttonTexturePath;
|
||||
set
|
||||
{
|
||||
_buttonTexturePath = value;
|
||||
ButtonRect.Texture = Theme.ResolveTexture(_buttonTexturePath);
|
||||
}
|
||||
}
|
||||
|
||||
public Texture StorageTexture => Theme.ResolveTexture(StorageTexturePath);
|
||||
|
||||
private string _storageTexturePath = "";
|
||||
public string StorageTexturePath
|
||||
{
|
||||
get => _buttonTexturePath;
|
||||
set
|
||||
{
|
||||
_storageTexturePath = value;
|
||||
StorageButton.TextureNormal = Theme.ResolveTexture(_storageTexturePath);
|
||||
}
|
||||
}
|
||||
|
||||
private string _highlightTexturePath = "";
|
||||
public string HighlightTexturePath
|
||||
{
|
||||
get => _highlightTexturePath;
|
||||
set
|
||||
{
|
||||
_highlightTexturePath = value;
|
||||
HighlightRect.Texture = Theme.ResolveTexture(_highlightTexturePath);
|
||||
}
|
||||
}
|
||||
|
||||
public event Action<GUIBoundKeyEventArgs, SlotControl>? Pressed;
|
||||
public event Action<GUIBoundKeyEventArgs, SlotControl>? Unpressed;
|
||||
public event Action<GUIBoundKeyEventArgs, SlotControl>? StoragePressed;
|
||||
public event Action<GUIMouseHoverEventArgs, SlotControl>? Hover;
|
||||
|
||||
public bool EntityHover => HoverSpriteView.Sprite != null;
|
||||
public bool MouseIsHovering;
|
||||
|
||||
public SlotControl()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
Name = "SlotButton_null";
|
||||
MinSize = (64, 64);
|
||||
AddChild(ButtonRect = new TextureRect
|
||||
{
|
||||
TextureScale = (2, 2),
|
||||
MouseFilter = MouseFilterMode.Stop
|
||||
});
|
||||
AddChild(HighlightRect = new TextureRect
|
||||
{
|
||||
Visible = false,
|
||||
TextureScale = (2, 2),
|
||||
MouseFilter = MouseFilterMode.Ignore
|
||||
});
|
||||
|
||||
ButtonRect.OnKeyBindDown += OnButtonPressed;
|
||||
ButtonRect.OnKeyBindUp += OnButtonUnpressed;
|
||||
|
||||
AddChild(SpriteView = new SpriteView
|
||||
{
|
||||
Scale = (2, 2),
|
||||
OverrideDirection = Direction.South
|
||||
});
|
||||
|
||||
AddChild(HoverSpriteView = new SpriteView
|
||||
{
|
||||
Scale = (2, 2),
|
||||
OverrideDirection = Direction.South
|
||||
});
|
||||
|
||||
AddChild(StorageButton = new TextureButton
|
||||
{
|
||||
Scale = (0.75f, 0.75f),
|
||||
HorizontalAlignment = HAlignment.Right,
|
||||
VerticalAlignment = VAlignment.Bottom,
|
||||
Visible = false,
|
||||
});
|
||||
|
||||
StorageButton.OnKeyBindDown += args =>
|
||||
{
|
||||
if (args.Function != EngineKeyFunctions.UIClick)
|
||||
{
|
||||
OnButtonPressed(args);
|
||||
}
|
||||
};
|
||||
|
||||
StorageButton.OnPressed += OnStorageButtonPressed;
|
||||
|
||||
ButtonRect.OnMouseEntered += _ =>
|
||||
{
|
||||
MouseIsHovering = true;
|
||||
};
|
||||
ButtonRect.OnMouseEntered += OnButtonHover;
|
||||
|
||||
ButtonRect.OnMouseExited += _ =>
|
||||
{
|
||||
MouseIsHovering = false;
|
||||
ClearHover();
|
||||
};
|
||||
|
||||
AddChild(CooldownDisplay = new CooldownGraphic
|
||||
{
|
||||
Visible = false,
|
||||
});
|
||||
|
||||
AddChild(BlockedRect = new TextureRect
|
||||
{
|
||||
TextureScale = (2, 2),
|
||||
MouseFilter = MouseFilterMode.Stop,
|
||||
Visible = false
|
||||
});
|
||||
|
||||
HighlightTexturePath = "slot_highlight";
|
||||
BlockedTexturePath = "blocked";
|
||||
}
|
||||
|
||||
public void ClearHover()
|
||||
{
|
||||
if (!EntityHover)
|
||||
return;
|
||||
|
||||
var tempQualifier = HoverSpriteView.Sprite;
|
||||
if (tempQualifier != null)
|
||||
{
|
||||
IoCManager.Resolve<IEntityManager>().DeleteEntity(tempQualifier.Owner);
|
||||
}
|
||||
|
||||
HoverSpriteView.Sprite = null;
|
||||
}
|
||||
|
||||
private void OnButtonPressed(GUIBoundKeyEventArgs args)
|
||||
{
|
||||
Pressed?.Invoke(args, this);
|
||||
}
|
||||
|
||||
private void OnButtonUnpressed(GUIBoundKeyEventArgs args)
|
||||
{
|
||||
Unpressed?.Invoke(args, this);
|
||||
}
|
||||
|
||||
private void OnStorageButtonPressed(BaseButton.ButtonEventArgs args)
|
||||
{
|
||||
if (args.Event.Function == EngineKeyFunctions.UIClick)
|
||||
{
|
||||
StoragePressed?.Invoke(args.Event, this);
|
||||
}
|
||||
else
|
||||
{
|
||||
Pressed?.Invoke(args.Event, this);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnButtonHover(GUIMouseHoverEventArgs args)
|
||||
{
|
||||
Hover?.Invoke(args, this);
|
||||
}
|
||||
|
||||
protected override void OnThemeUpdated()
|
||||
{
|
||||
StorageButton.TextureNormal = Theme.ResolveTexture(_storageTexturePath);
|
||||
ButtonRect.Texture = Theme.ResolveTexture(_buttonTexturePath);
|
||||
HighlightRect.Texture = Theme.ResolveTexture(_highlightTexturePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
20
Content.Client/UserInterface/Screens/DefaultGameScreen.xaml
Normal file
20
Content.Client/UserInterface/Screens/DefaultGameScreen.xaml
Normal file
@@ -0,0 +1,20 @@
|
||||
<screens:DefaultGameScreen
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:screens="clr-namespace:Content.Client.UserInterface.Screens"
|
||||
xmlns:menuBar="clr-namespace:Content.Client.UserInterface.Systems.MenuBar.Widgets"
|
||||
xmlns:actions="clr-namespace:Content.Client.UserInterface.Systems.Actions.Widgets"
|
||||
xmlns:chat="clr-namespace:Content.Client.UserInterface.Systems.Chat.Widgets"
|
||||
xmlns:alerts="clr-namespace:Content.Client.UserInterface.Systems.Alerts.Widgets"
|
||||
xmlns:widgets="clr-namespace:Content.Client.UserInterface.Systems.Ghost.Widgets"
|
||||
xmlns:hotbar="clr-namespace:Content.Client.UserInterface.Systems.Hotbar.Widgets"
|
||||
Name="DefaultHud"
|
||||
VerticalExpand="False"
|
||||
VerticalAlignment="Bottom"
|
||||
HorizontalAlignment="Center">
|
||||
<menuBar:GameTopMenuBar Name="TopBar" Access="Protected" />
|
||||
<widgets:GhostGui Name="Ghost" Access="Protected" />
|
||||
<hotbar:HotbarGui Name="Hotbar" Access="Protected" />
|
||||
<actions:ActionsBar Name="Actions" Access="Protected" />
|
||||
<chat:ResizableChatBox Name="Chat" Access="Protected" Main="True" />
|
||||
<alerts:AlertsUI Name="Alerts" Access="Protected" />
|
||||
</screens:DefaultGameScreen>
|
||||
@@ -0,0 +1,31 @@
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.UserInterface.Screens;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class DefaultGameScreen : UIScreen
|
||||
{
|
||||
public DefaultGameScreen()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
AutoscaleMaxResolution = new Vector2i(1080, 770);
|
||||
|
||||
SetAnchorAndMarginPreset(TopBar, LayoutPreset.TopLeft, margin: 10);
|
||||
SetAnchorAndMarginPreset(Actions, LayoutPreset.BottomLeft, margin: 10);
|
||||
SetAnchorAndMarginPreset(Ghost, LayoutPreset.BottomWide, margin: 80);
|
||||
SetAnchorAndMarginPreset(Hotbar, LayoutPreset.BottomWide, margin: 5);
|
||||
SetAnchorAndMarginPreset(Chat, LayoutPreset.TopRight, margin: 10);
|
||||
SetAnchorAndMarginPreset(Alerts, LayoutPreset.TopRight, margin: 10);
|
||||
|
||||
Chat.OnResized += ChatOnResized;
|
||||
}
|
||||
|
||||
private void ChatOnResized()
|
||||
{
|
||||
var marginBottom = Chat.GetValue<float>(MarginBottomProperty);
|
||||
SetMarginTop(Alerts, marginBottom);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,676 @@
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using Content.Client.Actions;
|
||||
using Content.Client.DragDrop;
|
||||
using Content.Client.Gameplay;
|
||||
using Content.Client.Hands;
|
||||
using Content.Client.Outline;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Client.UserInterface.Systems.Actions.Controls;
|
||||
using Content.Client.UserInterface.Systems.Actions.Widgets;
|
||||
using Content.Client.UserInterface.Systems.Actions.Windows;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Actions.ActionTypes;
|
||||
using Content.Shared.Input;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controllers;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Input.Binding;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using static Content.Client.Actions.ActionsSystem;
|
||||
using static Content.Client.UserInterface.Systems.Actions.Windows.ActionsWindow;
|
||||
using static Robust.Client.UserInterface.Control;
|
||||
using static Robust.Client.UserInterface.Controls.BaseButton;
|
||||
using static Robust.Client.UserInterface.Controls.LineEdit;
|
||||
using static Robust.Client.UserInterface.Controls.MultiselectOptionButton<
|
||||
Content.Client.UserInterface.Systems.Actions.Windows.ActionsWindow.Filters>;
|
||||
using static Robust.Client.UserInterface.Controls.TextureRect;
|
||||
using static Robust.Shared.Input.Binding.PointerInputCmdHandler;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Actions;
|
||||
|
||||
public sealed class ActionUIController : UIController, IOnStateChanged<GameplayState>, IOnSystemChanged<ActionsSystem>
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entities = default!;
|
||||
[Dependency] private readonly IOverlayManager _overlays = default!;
|
||||
|
||||
[UISystemDependency] private readonly ActionsSystem _actionsSystem = default!;
|
||||
[UISystemDependency] private readonly InteractionOutlineSystem _interactionOutline = default!;
|
||||
[UISystemDependency] private readonly TargetOutlineSystem _targetOutline = default!;
|
||||
|
||||
private const int DefaultPageIndex = 0;
|
||||
private ActionButtonContainer? _container;
|
||||
private readonly List<ActionPage> _pages = new();
|
||||
private int _currentPageIndex = DefaultPageIndex;
|
||||
private readonly DragDropHelper<ActionButton> _menuDragHelper;
|
||||
private readonly TextureRect _dragShadow;
|
||||
private ActionsWindow? _window;
|
||||
|
||||
private ActionsBar? _actionsBar;
|
||||
private MenuButton? _actionButton;
|
||||
private ActionPage CurrentPage => _pages[_currentPageIndex];
|
||||
|
||||
public bool IsDragging => _menuDragHelper.IsDragging;
|
||||
|
||||
/// <summary>
|
||||
/// Action slot we are currently selecting a target for.
|
||||
/// </summary>
|
||||
public ActionButton? SelectingTargetFor { get; private set; }
|
||||
|
||||
public ActionUIController()
|
||||
{
|
||||
_menuDragHelper = new DragDropHelper<ActionButton>(OnMenuBeginDrag, OnMenuContinueDrag, OnMenuEndDrag);
|
||||
_dragShadow = new TextureRect
|
||||
{
|
||||
MinSize = (64, 64),
|
||||
Stretch = StretchMode.Scale,
|
||||
Visible = false,
|
||||
SetSize = (64, 64),
|
||||
MouseFilter = MouseFilterMode.Ignore
|
||||
};
|
||||
|
||||
var pageCount = ContentKeyFunctions.GetLoadoutBoundKeys().Length;
|
||||
var buttonCount = ContentKeyFunctions.GetHotbarBoundKeys().Length;
|
||||
for (var i = 0; i < pageCount; i++)
|
||||
{
|
||||
var page = new ActionPage(buttonCount);
|
||||
_pages.Add(page);
|
||||
}
|
||||
}
|
||||
|
||||
public void OnStateEntered(GameplayState state)
|
||||
{
|
||||
DebugTools.Assert(_window == null);
|
||||
_window = UIManager.CreateWindow<ActionsWindow>();
|
||||
_actionButton = UIManager.GetActiveUIWidget<MenuBar.Widgets.GameTopMenuBar>().ActionButton;
|
||||
LayoutContainer.SetAnchorPreset(_window, LayoutContainer.LayoutPreset.CenterTop);
|
||||
_window.OnClose += () => { _actionButton.Pressed = false; };
|
||||
_window.OnOpen += () => { _actionButton.Pressed = true; };
|
||||
_window.ClearButton.OnPressed += OnClearPressed;
|
||||
_window.SearchBar.OnTextChanged += OnSearchChanged;
|
||||
_window.FilterButton.OnItemSelected += OnFilterSelected;
|
||||
UpdateFilterLabel();
|
||||
SearchAndDisplay();
|
||||
|
||||
_actionsBar = UIManager.GetActiveUIWidget<ActionsBar>();
|
||||
_actionsBar.PageButtons.LeftArrow.OnPressed += OnLeftArrowPressed;
|
||||
_actionsBar.PageButtons.RightArrow.OnPressed += OnRightArrowPressed;
|
||||
_actionButton.OnPressed += ActionButtonPressed;
|
||||
_dragShadow.Orphan();
|
||||
UIManager.PopupRoot.AddChild(_dragShadow);
|
||||
|
||||
var builder = CommandBinds.Builder;
|
||||
var hotbarKeys = ContentKeyFunctions.GetHotbarBoundKeys();
|
||||
for (var i = 0; i < hotbarKeys.Length; i++)
|
||||
{
|
||||
var boundId = i; // This is needed, because the lambda captures it.
|
||||
var boundKey = hotbarKeys[i];
|
||||
builder = builder.Bind(boundKey, new PointerInputCmdHandler((in PointerInputCmdArgs args) =>
|
||||
{
|
||||
if (args.State != BoundKeyState.Up)
|
||||
return false;
|
||||
|
||||
TriggerAction(boundId);
|
||||
return true;
|
||||
}, false));
|
||||
}
|
||||
|
||||
var loadoutKeys = ContentKeyFunctions.GetLoadoutBoundKeys();
|
||||
for (var i = 0; i < loadoutKeys.Length; i++)
|
||||
{
|
||||
var boundId = i; // This is needed, because the lambda captures it.
|
||||
var boundKey = loadoutKeys[i];
|
||||
builder = builder.Bind(boundKey,
|
||||
InputCmdHandler.FromDelegate(_ => ChangePage(boundId)));
|
||||
}
|
||||
|
||||
builder
|
||||
.Bind(ContentKeyFunctions.OpenActionsMenu,
|
||||
InputCmdHandler.FromDelegate(_ => ToggleWindow()))
|
||||
.Register<ActionUIController>();
|
||||
}
|
||||
|
||||
public void OnStateExited(GameplayState state)
|
||||
{
|
||||
if (_window != null)
|
||||
{
|
||||
_window.Dispose();
|
||||
_window = null;
|
||||
}
|
||||
|
||||
if (_actionsBar != null)
|
||||
{
|
||||
_actionsBar.PageButtons.LeftArrow.OnPressed -= OnLeftArrowPressed;
|
||||
_actionsBar.PageButtons.RightArrow.OnPressed -= OnRightArrowPressed;
|
||||
}
|
||||
|
||||
if (_actionButton != null)
|
||||
{
|
||||
_actionButton.OnPressed -= ActionButtonPressed;
|
||||
_actionButton.Pressed = false;
|
||||
}
|
||||
|
||||
CommandBinds.Unregister<ActionUIController>();
|
||||
}
|
||||
|
||||
private void TriggerAction(int index)
|
||||
{
|
||||
if (CurrentPage[index] is not { } type)
|
||||
return;
|
||||
|
||||
_actionsSystem.TriggerAction(type);
|
||||
}
|
||||
|
||||
private void ChangePage(int index)
|
||||
{
|
||||
var lastPage = _pages.Count - 1;
|
||||
if (index < 0)
|
||||
{
|
||||
index = lastPage;
|
||||
}
|
||||
else if (index > lastPage)
|
||||
{
|
||||
index = 0;
|
||||
}
|
||||
|
||||
_currentPageIndex = index;
|
||||
var page = _pages[_currentPageIndex];
|
||||
_container?.SetActionData(page);
|
||||
|
||||
_actionsBar!.PageButtons.Label.Text = $"{_currentPageIndex + 1}";
|
||||
}
|
||||
|
||||
private void OnLeftArrowPressed(ButtonEventArgs args)
|
||||
{
|
||||
ChangePage(_currentPageIndex - 1);
|
||||
}
|
||||
|
||||
private void OnRightArrowPressed(ButtonEventArgs args)
|
||||
{
|
||||
ChangePage(_currentPageIndex + 1);
|
||||
}
|
||||
|
||||
private void ActionButtonPressed(ButtonEventArgs args)
|
||||
{
|
||||
ToggleWindow();
|
||||
}
|
||||
|
||||
private void ToggleWindow()
|
||||
{
|
||||
if (_window == null)
|
||||
return;
|
||||
|
||||
if (_window.IsOpen)
|
||||
{
|
||||
_window.Close();
|
||||
return;
|
||||
}
|
||||
|
||||
_window.Open();
|
||||
}
|
||||
|
||||
private void UpdateFilterLabel()
|
||||
{
|
||||
if (_window == null)
|
||||
return;
|
||||
|
||||
if (_window.FilterButton.SelectedKeys.Count == 0)
|
||||
{
|
||||
_window.FilterLabel.Visible = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
_window.FilterLabel.Visible = true;
|
||||
_window.FilterLabel.Text = Loc.GetString("ui-actionmenu-filter-label",
|
||||
("selectedLabels", string.Join(", ", _window.FilterButton.SelectedLabels)));
|
||||
}
|
||||
}
|
||||
|
||||
private bool MatchesFilter(ActionType action, Filters filter)
|
||||
{
|
||||
return filter switch
|
||||
{
|
||||
Filters.Enabled => action.Enabled,
|
||||
Filters.Item => action.Provider != null && action.Provider != _actionsSystem.PlayerActions?.Owner,
|
||||
Filters.Innate => action.Provider == null || action.Provider == _actionsSystem.PlayerActions?.Owner,
|
||||
Filters.Instant => action is InstantAction,
|
||||
Filters.Targeted => action is TargetedAction,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(filter), filter, null)
|
||||
};
|
||||
}
|
||||
|
||||
private void ClearList()
|
||||
{
|
||||
_window?.ResultsGrid.RemoveAllChildren();
|
||||
}
|
||||
|
||||
private void PopulateActions(IEnumerable<ActionType> actions)
|
||||
{
|
||||
if (_window == null)
|
||||
return;
|
||||
|
||||
ClearList();
|
||||
|
||||
foreach (var action in actions)
|
||||
{
|
||||
var button = new ActionButton {Locked = true};
|
||||
|
||||
button.UpdateData(_entities, action);
|
||||
button.ActionPressed += OnWindowActionPressed;
|
||||
button.ActionUnpressed += OnWindowActionUnPressed;
|
||||
button.ActionFocusExited += OnWindowActionFocusExisted;
|
||||
|
||||
_window.ResultsGrid.AddChild(button);
|
||||
}
|
||||
}
|
||||
|
||||
private void SearchAndDisplay()
|
||||
{
|
||||
if (_window == null)
|
||||
return;
|
||||
|
||||
var search = _window.SearchBar.Text;
|
||||
var filters = _window.FilterButton.SelectedKeys;
|
||||
|
||||
IEnumerable<ActionType>? actions = _actionsSystem.PlayerActions?.Actions;
|
||||
actions ??= Array.Empty<ActionType>();
|
||||
|
||||
if (filters.Count == 0 && string.IsNullOrWhiteSpace(search))
|
||||
{
|
||||
PopulateActions(actions);
|
||||
return;
|
||||
}
|
||||
|
||||
actions = actions.Where(action =>
|
||||
{
|
||||
if (filters.Count > 0 && filters.Any(filter => !MatchesFilter(action, filter)))
|
||||
return false;
|
||||
|
||||
if (action.Keywords.Any(keyword => search.Contains(keyword, StringComparison.OrdinalIgnoreCase)))
|
||||
return true;
|
||||
|
||||
if (action.DisplayName.Contains((string) search, StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
|
||||
if (action.Provider == null || action.Provider == _actionsSystem.PlayerActions?.Owner)
|
||||
return false;
|
||||
|
||||
var name = _entities.GetComponent<MetaDataComponent>(action.Provider.Value).EntityName;
|
||||
return name.Contains((string) search, StringComparison.OrdinalIgnoreCase);
|
||||
});
|
||||
|
||||
PopulateActions(actions);
|
||||
}
|
||||
|
||||
private void SetAction(ActionButton button, ActionType? type)
|
||||
{
|
||||
int position;
|
||||
|
||||
if (type == null)
|
||||
{
|
||||
button.ClearData();
|
||||
if (_container?.TryGetButtonIndex(button, out position) ?? false)
|
||||
{
|
||||
CurrentPage[position] = type;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (button.TryReplaceWith(_entities, type) &&
|
||||
_container != null &&
|
||||
_container.TryGetButtonIndex(button, out position))
|
||||
{
|
||||
CurrentPage[position] = type;
|
||||
}
|
||||
}
|
||||
|
||||
private void DragAction()
|
||||
{
|
||||
if (UIManager.CurrentlyHovered is ActionButton button)
|
||||
{
|
||||
if (!_menuDragHelper.IsDragging || _menuDragHelper.Dragged?.Action is not { } type)
|
||||
{
|
||||
_menuDragHelper.EndDrag();
|
||||
return;
|
||||
}
|
||||
|
||||
SetAction(button, type);
|
||||
}
|
||||
|
||||
if (_menuDragHelper.Dragged is {Parent: ActionButtonContainer} old)
|
||||
{
|
||||
SetAction(old, null);
|
||||
}
|
||||
|
||||
_menuDragHelper.EndDrag();
|
||||
}
|
||||
|
||||
private void OnClearPressed(ButtonEventArgs args)
|
||||
{
|
||||
if (_window == null)
|
||||
return;
|
||||
|
||||
_window.SearchBar.Clear();
|
||||
_window.FilterButton.DeselectAll();
|
||||
UpdateFilterLabel();
|
||||
SearchAndDisplay();
|
||||
}
|
||||
|
||||
private void OnSearchChanged(LineEditEventArgs args)
|
||||
{
|
||||
SearchAndDisplay();
|
||||
}
|
||||
|
||||
private void OnFilterSelected(ItemPressedEventArgs args)
|
||||
{
|
||||
UpdateFilterLabel();
|
||||
SearchAndDisplay();
|
||||
}
|
||||
|
||||
private void OnWindowActionPressed(GUIBoundKeyEventArgs args, ActionButton action)
|
||||
{
|
||||
if (args.Function != EngineKeyFunctions.UIClick && args.Function != EngineKeyFunctions.Use)
|
||||
return;
|
||||
|
||||
_menuDragHelper.MouseDown(action);
|
||||
args.Handle();
|
||||
}
|
||||
|
||||
private void OnWindowActionUnPressed(GUIBoundKeyEventArgs args, ActionButton dragged)
|
||||
{
|
||||
if (args.Function != EngineKeyFunctions.UIClick && args.Function != EngineKeyFunctions.Use)
|
||||
return;
|
||||
|
||||
DragAction();
|
||||
args.Handle();
|
||||
}
|
||||
|
||||
private void OnWindowActionFocusExisted(ActionButton button)
|
||||
{
|
||||
_menuDragHelper.EndDrag();
|
||||
}
|
||||
|
||||
private void OnActionPressed(GUIBoundKeyEventArgs args, ActionButton button)
|
||||
{
|
||||
if (args.Function == EngineKeyFunctions.UIClick)
|
||||
{
|
||||
_menuDragHelper.MouseDown(button);
|
||||
args.Handle();
|
||||
}
|
||||
else if (args.Function == EngineKeyFunctions.UIRightClick)
|
||||
{
|
||||
SetAction(button, null);
|
||||
args.Handle();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnActionUnpressed(GUIBoundKeyEventArgs args, ActionButton button)
|
||||
{
|
||||
if (args.Function != EngineKeyFunctions.UIClick)
|
||||
return;
|
||||
|
||||
if (UIManager.CurrentlyHovered == button)
|
||||
{
|
||||
if (button.Action is not InstantAction)
|
||||
{
|
||||
// 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
|
||||
ToggleTargeting(button);
|
||||
return;
|
||||
}
|
||||
|
||||
_actionsSystem.TriggerAction(button.Action);
|
||||
_menuDragHelper.EndDrag();
|
||||
}
|
||||
else
|
||||
{
|
||||
DragAction();
|
||||
}
|
||||
|
||||
args.Handle();
|
||||
}
|
||||
|
||||
private bool OnMenuBeginDrag()
|
||||
{
|
||||
_dragShadow.Texture = _menuDragHelper.Dragged?.IconTexture;
|
||||
LayoutContainer.SetPosition(_dragShadow, UIManager.MousePositionScaled.Position - (32, 32));
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool OnMenuContinueDrag(float frameTime)
|
||||
{
|
||||
LayoutContainer.SetPosition(_dragShadow, UIManager.MousePositionScaled.Position - (32, 32));
|
||||
_dragShadow.Visible = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnMenuEndDrag()
|
||||
{
|
||||
_dragShadow.Visible = false;
|
||||
}
|
||||
|
||||
public void RegisterActionContainer(ActionButtonContainer container)
|
||||
{
|
||||
if (_container != null)
|
||||
{
|
||||
Logger.Warning("Action container already defined for UI controller");
|
||||
return;
|
||||
}
|
||||
|
||||
_container = container;
|
||||
_container.ActionPressed += OnActionPressed;
|
||||
_container.ActionUnpressed += OnActionUnpressed;
|
||||
}
|
||||
|
||||
public void ClearActions()
|
||||
{
|
||||
_container?.ClearActionData();
|
||||
}
|
||||
|
||||
private void AssignSlots(List<SlotAssignment> assignments)
|
||||
{
|
||||
foreach (ref var assignment in CollectionsMarshal.AsSpan(assignments))
|
||||
{
|
||||
_pages[assignment.Hotbar][assignment.Slot] = assignment.Action;
|
||||
}
|
||||
|
||||
_container?.SetActionData(_pages[_currentPageIndex]);
|
||||
}
|
||||
|
||||
public void RemoveActionContainer()
|
||||
{
|
||||
_container = null;
|
||||
}
|
||||
|
||||
public void OnSystemLoaded(ActionsSystem system)
|
||||
{
|
||||
_actionsSystem.OnLinkActions += OnComponentLinked;
|
||||
_actionsSystem.OnUnlinkActions += OnComponentUnlinked;
|
||||
_actionsSystem.ClearAssignments += ClearActions;
|
||||
_actionsSystem.AssignSlot += AssignSlots;
|
||||
}
|
||||
|
||||
public void OnSystemUnloaded(ActionsSystem system)
|
||||
{
|
||||
_actionsSystem.OnLinkActions -= OnComponentLinked;
|
||||
_actionsSystem.OnUnlinkActions -= OnComponentUnlinked;
|
||||
_actionsSystem.ClearAssignments -= ClearActions;
|
||||
_actionsSystem.AssignSlot -= AssignSlots;
|
||||
}
|
||||
|
||||
public override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
_menuDragHelper.Update(args.DeltaSeconds);
|
||||
}
|
||||
|
||||
private void OnComponentLinked(ActionsComponent component)
|
||||
{
|
||||
LoadDefaultActions(component);
|
||||
_container?.SetActionData(_pages[DefaultPageIndex]);
|
||||
}
|
||||
|
||||
private void OnComponentUnlinked()
|
||||
{
|
||||
_container?.ClearActionData();
|
||||
//TODO: Clear button data
|
||||
}
|
||||
|
||||
private void LoadDefaultActions(ActionsComponent component)
|
||||
{
|
||||
var actions = component.Actions.Where(actionType => actionType.AutoPopulate).ToList();
|
||||
|
||||
var offset = 0;
|
||||
var totalPages = _pages.Count;
|
||||
var pagesLeft = totalPages;
|
||||
var currentPage = DefaultPageIndex;
|
||||
while (pagesLeft > 0)
|
||||
{
|
||||
var page = _pages[currentPage];
|
||||
var pageSize = page.Size;
|
||||
|
||||
for (var slot = 0; slot < pageSize; slot++)
|
||||
{
|
||||
var actionIndex = slot + offset;
|
||||
if (actionIndex < actions.Count)
|
||||
{
|
||||
page[slot] = actions[slot + offset];
|
||||
}
|
||||
else
|
||||
{
|
||||
page[slot] = null;
|
||||
}
|
||||
}
|
||||
|
||||
offset += pageSize;
|
||||
currentPage++;
|
||||
if (currentPage == totalPages)
|
||||
{
|
||||
currentPage = 0;
|
||||
}
|
||||
|
||||
pagesLeft--;
|
||||
}
|
||||
}
|
||||
|
||||
/// <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(ActionButton 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(ActionButton actionSlot)
|
||||
{
|
||||
if (actionSlot.Action == null)
|
||||
return;
|
||||
|
||||
// If we were targeting something else we should stop
|
||||
StopTargeting();
|
||||
|
||||
SelectingTargetFor = actionSlot;
|
||||
|
||||
if (actionSlot.Action is not TargetedAction action)
|
||||
return;
|
||||
|
||||
// override "held-item" overlay
|
||||
if (action.TargetingIndicator && _overlays.TryGetOverlay<ShowHandItemOverlay>(out var handOverlay))
|
||||
{
|
||||
if (action.ItemIconStyle == ItemActionIconStyle.BigItem && action.Provider != null)
|
||||
{
|
||||
handOverlay.EntityOverride = action.Provider;
|
||||
}
|
||||
else if (action.Toggled && action.IconOn != null)
|
||||
handOverlay.IconOverride = action.IconOn.Frame0();
|
||||
else if (action.Icon != null)
|
||||
handOverlay.IconOverride = action.Icon.Frame0();
|
||||
}
|
||||
|
||||
// TODO: allow world-targets to check valid positions. E.g., maybe:
|
||||
// - Draw a red/green ghost entity
|
||||
// - Add a yes/no checkmark where the HandItemOverlay usually is
|
||||
|
||||
// Highlight valid entity targets
|
||||
if (action is not EntityTargetAction entityAction)
|
||||
return;
|
||||
|
||||
Func<EntityUid, bool>? predicate = null;
|
||||
|
||||
if (!entityAction.CanTargetSelf)
|
||||
predicate = e => e != entityAction.AttachedEntity;
|
||||
|
||||
var range = entityAction.CheckCanAccess ? action.Range : -1;
|
||||
|
||||
_interactionOutline.SetEnabled(false);
|
||||
_targetOutline.Enable(range, entityAction.CheckCanAccess, predicate, entityAction.Whitelist, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Switch out of targeting mode if currently selecting target for an action
|
||||
/// </summary>
|
||||
public void StopTargeting()
|
||||
{
|
||||
if (SelectingTargetFor == null)
|
||||
return;
|
||||
|
||||
SelectingTargetFor = null;
|
||||
_targetOutline.Disable();
|
||||
_interactionOutline.SetEnabled(true);
|
||||
|
||||
if (!_overlays.TryGetOverlay<ShowHandItemOverlay>(out var handOverlay) || handOverlay == null)
|
||||
return;
|
||||
|
||||
handOverlay.IconOverride = null;
|
||||
handOverlay.EntityOverride = null;
|
||||
}
|
||||
|
||||
|
||||
//TODO: Serialize this shit
|
||||
private sealed class ActionPage
|
||||
{
|
||||
private readonly ActionType?[] _data;
|
||||
|
||||
public ActionPage(int size)
|
||||
{
|
||||
_data = new ActionType?[size];
|
||||
}
|
||||
|
||||
public ActionType? this[int index]
|
||||
{
|
||||
get => _data[index];
|
||||
set => _data[index] = value;
|
||||
}
|
||||
|
||||
public static implicit operator ActionType?[](ActionPage p)
|
||||
{
|
||||
return p._data.ToArray();
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
Array.Fill(_data, null);
|
||||
}
|
||||
|
||||
public int Size => _data.Length;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
using Content.Client.Actions.UI;
|
||||
using Content.Client.Cooldown;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Shared.Actions.ActionTypes;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Actions.Controls;
|
||||
|
||||
public sealed class ActionButton : Control
|
||||
{
|
||||
private ActionUIController Controller => UserInterfaceManager.GetUIController<ActionUIController>();
|
||||
private bool _beingHovered;
|
||||
private bool _depressed;
|
||||
private bool _toggled;
|
||||
|
||||
public BoundKeyFunction? KeyBind
|
||||
{
|
||||
set
|
||||
{
|
||||
_keybind = value;
|
||||
if (_keybind != null)
|
||||
{
|
||||
Label.Text = BoundKeyHelper.ShortKeyName(_keybind.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private BoundKeyFunction? _keybind;
|
||||
|
||||
public readonly TextureRect Button;
|
||||
public readonly PanelContainer HighlightRect;
|
||||
public readonly TextureRect Icon;
|
||||
public readonly Label Label;
|
||||
public readonly SpriteView Sprite;
|
||||
public readonly CooldownGraphic Cooldown;
|
||||
|
||||
public Texture? IconTexture
|
||||
{
|
||||
get => Icon.Texture;
|
||||
private set => Icon.Texture = value;
|
||||
}
|
||||
|
||||
public ActionType? Action { get; private set; }
|
||||
public bool Locked { get; set; }
|
||||
|
||||
public event Action<GUIBoundKeyEventArgs, ActionButton>? ActionPressed;
|
||||
public event Action<GUIBoundKeyEventArgs, ActionButton>? ActionUnpressed;
|
||||
public event Action<ActionButton>? ActionFocusExited;
|
||||
|
||||
public ActionButton()
|
||||
{
|
||||
MouseFilter = MouseFilterMode.Pass;
|
||||
Button = new TextureRect
|
||||
{
|
||||
Name = "Button",
|
||||
TextureScale = new Vector2(2, 2)
|
||||
};
|
||||
HighlightRect = new PanelContainer
|
||||
{
|
||||
StyleClasses = {StyleNano.StyleClassHandSlotHighlight},
|
||||
MinSize = (32, 32),
|
||||
Visible = false
|
||||
};
|
||||
Icon = new TextureRect
|
||||
{
|
||||
Name = "Icon",
|
||||
TextureScale = new Vector2(2, 2),
|
||||
MaxSize = (64, 64),
|
||||
Stretch = TextureRect.StretchMode.Scale
|
||||
};
|
||||
Label = new Label
|
||||
{
|
||||
Name = "Label",
|
||||
HorizontalAlignment = HAlignment.Left,
|
||||
VerticalAlignment = VAlignment.Top,
|
||||
Margin = new Thickness(5, 0, 0, 0)
|
||||
};
|
||||
Sprite = new SpriteView
|
||||
{
|
||||
Name = "Sprite",
|
||||
OverrideDirection = Direction.South
|
||||
};
|
||||
Cooldown = new CooldownGraphic {Visible = false};
|
||||
|
||||
AddChild(Button);
|
||||
AddChild(HighlightRect);
|
||||
AddChild(Icon);
|
||||
AddChild(Label);
|
||||
AddChild(Sprite);
|
||||
AddChild(Cooldown);
|
||||
|
||||
Button.Modulate = new Color(255, 255, 255, 150);
|
||||
Icon.Modulate = new Color(255, 255, 255, 150);
|
||||
|
||||
OnThemeUpdated();
|
||||
OnThemeUpdated();
|
||||
|
||||
OnKeyBindDown += args =>
|
||||
{
|
||||
Depress(args, true);
|
||||
OnPressed(args);
|
||||
};
|
||||
OnKeyBindUp += args =>
|
||||
{
|
||||
Depress(args, false);
|
||||
OnUnpressed(args);
|
||||
};
|
||||
|
||||
TooltipDelay = 0.5f;
|
||||
TooltipSupplier = SupplyTooltip;
|
||||
}
|
||||
|
||||
protected override void OnThemeUpdated()
|
||||
{
|
||||
Button.Texture = Theme.ResolveTexture("SlotBackground");
|
||||
Label.FontColorOverride = Theme.ResolveColorOrSpecified("whiteText");
|
||||
}
|
||||
|
||||
private void OnPressed(GUIBoundKeyEventArgs args)
|
||||
{
|
||||
ActionPressed?.Invoke(args, this);
|
||||
}
|
||||
|
||||
private void OnUnpressed(GUIBoundKeyEventArgs args)
|
||||
{
|
||||
ActionUnpressed?.Invoke(args, this);
|
||||
}
|
||||
|
||||
private Control? SupplyTooltip(Control sender)
|
||||
{
|
||||
if (Action == null)
|
||||
return null;
|
||||
|
||||
var name = FormattedMessage.FromMarkupPermissive(Loc.GetString(Action.DisplayName));
|
||||
var decr = FormattedMessage.FromMarkupPermissive(Loc.GetString(Action.Description));
|
||||
|
||||
return new ActionAlertTooltip(name, decr);
|
||||
}
|
||||
|
||||
protected override void ControlFocusExited()
|
||||
{
|
||||
ActionFocusExited?.Invoke(this);
|
||||
}
|
||||
|
||||
public bool TryReplaceWith(IEntityManager entityManager, ActionType action)
|
||||
{
|
||||
if (Locked)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
UpdateData(entityManager, action);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void UpdateData(IEntityManager entityManager, ActionType action)
|
||||
{
|
||||
Action = action;
|
||||
|
||||
if (action.Icon != null)
|
||||
{
|
||||
IconTexture = GetIcon();
|
||||
Sprite.Sprite = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (action.Provider == null ||
|
||||
!entityManager.TryGetComponent(action.Provider.Value, out SpriteComponent? sprite))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IconTexture = null;
|
||||
Sprite.Sprite = sprite;
|
||||
}
|
||||
|
||||
public void ClearData()
|
||||
{
|
||||
Action = null;
|
||||
IconTexture = null;
|
||||
Sprite.Sprite = null;
|
||||
Cooldown.Visible = false;
|
||||
Cooldown.Progress = 1;
|
||||
}
|
||||
|
||||
private Texture? GetIcon()
|
||||
{
|
||||
if (Action == null)
|
||||
return null;
|
||||
|
||||
return _toggled ? (Action.IconOn ?? Action.Icon)?.Frame0() : Action.Icon?.Frame0();
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
|
||||
if (Action?.Cooldown != null)
|
||||
{
|
||||
Cooldown.FromTime(Action.Cooldown.Value.Start, Action.Cooldown.Value.End);
|
||||
}
|
||||
|
||||
if (Action != null && _toggled != Action.Toggled)
|
||||
{
|
||||
_toggled = Action.Toggled;
|
||||
IconTexture = GetIcon();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void MouseEntered()
|
||||
{
|
||||
base.MouseEntered();
|
||||
|
||||
_beingHovered = true;
|
||||
DrawModeChanged();
|
||||
}
|
||||
|
||||
protected override void MouseExited()
|
||||
{
|
||||
base.MouseExited();
|
||||
|
||||
_beingHovered = false;
|
||||
DrawModeChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Press this button down. If it was depressed and now set to not depressed, will
|
||||
/// trigger the action.
|
||||
/// </summary>
|
||||
public void Depress(GUIBoundKeyEventArgs args, bool depress)
|
||||
{
|
||||
// action can still be toggled if it's allowed to stay selected
|
||||
if (Action is not {Enabled: true})
|
||||
return;
|
||||
|
||||
if (_depressed && !depress)
|
||||
{
|
||||
// fire the action
|
||||
OnUnpressed(args);
|
||||
}
|
||||
|
||||
_depressed = depress;
|
||||
DrawModeChanged();
|
||||
}
|
||||
|
||||
public void DrawModeChanged()
|
||||
{
|
||||
HighlightRect.Visible = _beingHovered;
|
||||
|
||||
// always show the normal empty button style if no action in this slot
|
||||
if (Action == null)
|
||||
{
|
||||
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassNormal);
|
||||
return;
|
||||
}
|
||||
|
||||
// show a hover only if the action is usable or another action is being dragged on top of this
|
||||
if (_beingHovered && (Controller.IsDragging || Action.Enabled))
|
||||
{
|
||||
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassHover);
|
||||
}
|
||||
|
||||
// it's only depress-able if it's usable, so if we're depressed
|
||||
// show the depressed style
|
||||
if (_depressed)
|
||||
{
|
||||
HighlightRect.Visible = false;
|
||||
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassPressed);
|
||||
return;
|
||||
}
|
||||
|
||||
// if it's toggled on, always show the toggled on style (currently same as depressed style)
|
||||
if (Action.Toggled || Controller.SelectingTargetFor == this)
|
||||
{
|
||||
// when there's a toggle sprite, we're showing that sprite instead of highlighting this slot
|
||||
SetOnlyStylePseudoClass(Action.IconOn != null
|
||||
? ContainerButton.StylePseudoClassNormal
|
||||
: ContainerButton.StylePseudoClassPressed);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Action.Enabled)
|
||||
{
|
||||
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassDisabled);
|
||||
return;
|
||||
}
|
||||
|
||||
SetOnlyStylePseudoClass(ContainerButton.StylePseudoClassNormal);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
using Content.Shared.Actions.ActionTypes;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Actions.Controls;
|
||||
|
||||
[Virtual]
|
||||
public class ActionButtonContainer : GridContainer
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
|
||||
public event Action<GUIBoundKeyEventArgs, ActionButton>? ActionPressed;
|
||||
public event Action<GUIBoundKeyEventArgs, ActionButton>? ActionUnpressed;
|
||||
public event Action<ActionButton>? ActionFocusExited;
|
||||
|
||||
public ActionButtonContainer()
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
UserInterfaceManager.GetUIController<ActionUIController>().RegisterActionContainer(this);
|
||||
}
|
||||
|
||||
public ActionButton this[int index]
|
||||
{
|
||||
get => (ActionButton) GetChild(index);
|
||||
set
|
||||
{
|
||||
AddChild(value);
|
||||
value.SetPositionInParent(index);
|
||||
value.ActionPressed += ActionPressed;
|
||||
value.ActionUnpressed += ActionUnpressed;
|
||||
value.ActionFocusExited += ActionFocusExited;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetActionData(params ActionType?[] actionTypes)
|
||||
{
|
||||
ClearActionData();
|
||||
|
||||
for (var i = 0; i < actionTypes.Length; i++)
|
||||
{
|
||||
var action = actionTypes[i];
|
||||
if (action == null)
|
||||
continue;
|
||||
|
||||
((ActionButton) GetChild(i)).UpdateData(_entityManager, action);
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearActionData()
|
||||
{
|
||||
foreach (var button in Children)
|
||||
{
|
||||
((ActionButton) button).ClearData();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ChildAdded(Control newChild)
|
||||
{
|
||||
base.ChildAdded(newChild);
|
||||
|
||||
if (newChild is not ActionButton button)
|
||||
return;
|
||||
|
||||
button.ActionPressed += ActionPressed;
|
||||
button.ActionUnpressed += ActionUnpressed;
|
||||
button.ActionFocusExited += ActionFocusExited;
|
||||
}
|
||||
|
||||
public bool TryGetButtonIndex(ActionButton button, out int position)
|
||||
{
|
||||
if (button.Parent != this)
|
||||
{
|
||||
position = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
position = button.GetPositionInParent();
|
||||
return true;
|
||||
}
|
||||
|
||||
public IEnumerable<ActionButton> GetButtons()
|
||||
{
|
||||
foreach (var control in Children)
|
||||
{
|
||||
if (control is ActionButton button)
|
||||
yield return button;
|
||||
}
|
||||
}
|
||||
|
||||
~ActionButtonContainer()
|
||||
{
|
||||
UserInterfaceManager.GetUIController<ActionUIController>().RemoveActionContainer();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<controls:ActionPageButtons
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Systems.Actions.Controls">
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Control HorizontalExpand="True" SizeFlagsStretchRatio="1"/>
|
||||
<TextureButton TexturePath="/Textures/Interface/Nano/left_arrow.svg.192dpi.png"
|
||||
SizeFlagsStretchRatio="1"
|
||||
Scale="0.5 0.5"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Name="LeftArrow"
|
||||
Access="Public"/>
|
||||
<Control HorizontalExpand="True" SizeFlagsStretchRatio="2"/>
|
||||
<Label Text="1" SizeFlagsStretchRatio="1" Name="Label" Access="Public" />
|
||||
<Control HorizontalExpand="True" SizeFlagsStretchRatio="2"/>
|
||||
<TextureButton TexturePath="/Textures/Interface/Nano/right_arrow.svg.192dpi.png"
|
||||
SizeFlagsStretchRatio="1"
|
||||
Scale="0.5 0.5"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Name="RightArrow"
|
||||
Access="Public"/>
|
||||
<Control HorizontalExpand="True" SizeFlagsStretchRatio="1"/>
|
||||
</BoxContainer>
|
||||
</controls:ActionPageButtons>
|
||||
@@ -0,0 +1,14 @@
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Actions.Controls;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class ActionPageButtons : Control
|
||||
{
|
||||
public ActionPageButtons()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<controls:ActionTooltip
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Systems.Actions.Controls"
|
||||
StyleClasses="StyleClassTooltipPanel">
|
||||
<BoxContainer Orientation="Vertical" RectClipContent="True">
|
||||
<RichTextLabel MaxWidth="350" StyleClasses="StyleClassTooltipActionTitle"/>
|
||||
<RichTextLabel MaxWidth="350" StyleClasses="StyleClassTooltipActionDescription"/>
|
||||
</BoxContainer>
|
||||
</controls:ActionTooltip>
|
||||
@@ -0,0 +1,14 @@
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Actions.Controls;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class ActionTooltip : PanelContainer
|
||||
{
|
||||
public ActionTooltip()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<widgets:ActionsBar
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:in="clr-namespace:Content.Shared.Input;assembly=Content.Shared"
|
||||
xmlns:widgets="clr-namespace:Content.Client.UserInterface.Systems.Actions.Widgets"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Systems.Actions.Controls"
|
||||
VerticalExpand="False"
|
||||
Orientation="Horizontal"
|
||||
HorizontalExpand="False"
|
||||
>
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<controls:ActionButtonContainer
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Name="ActionsContainer"
|
||||
Access="Public"/>
|
||||
<controls:ActionPageButtons Name="PageButtons" Access="Public"/>
|
||||
</BoxContainer>
|
||||
</widgets:ActionsBar>
|
||||
@@ -0,0 +1,34 @@
|
||||
using Content.Client.UserInterface.Systems.Actions.Controls;
|
||||
using Content.Shared.Input;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Input;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Actions.Widgets;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class ActionsBar : UIWidget
|
||||
{
|
||||
public ActionsBar()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
var keys = ContentKeyFunctions.GetHotbarBoundKeys();
|
||||
for (var index = 1; index < keys.Length; index++)
|
||||
{
|
||||
ActionsContainer.Children.Add(MakeButton(index));
|
||||
}
|
||||
ActionsContainer.Children.Add(MakeButton(0));
|
||||
|
||||
ActionButton MakeButton(int index)
|
||||
{
|
||||
var boundKey = keys[index];
|
||||
var button = new ActionButton();
|
||||
button.KeyBind = boundKey;
|
||||
button.Label.Text = index.ToString();
|
||||
return button;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<windows:ActionsWindow
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:windows="clr-namespace:Content.Client.UserInterface.Systems.Actions.Windows"
|
||||
Name="ActionsList"
|
||||
HorizontalExpand="True"
|
||||
Title="Actions"
|
||||
VerticalExpand="True"
|
||||
Resizable="True"
|
||||
MinHeight="300"
|
||||
MinWidth="300"
|
||||
>
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<BoxContainer Name="SearchContainer" Orientation="Horizontal">
|
||||
<LineEdit Name="SearchBar" Access="Public" StyleClasses="actionSearchBox" HorizontalExpand="True"
|
||||
PlaceHolder="{Loc ui-actionmenu-search-bar-placeholder-text}"/>
|
||||
</BoxContainer>
|
||||
<Button Name="ClearButton" Access="Public" Text="{Loc ui-actionmenu-clear-button}"/>
|
||||
<Label Name="FilterLabel" Access="Public"/>
|
||||
<ScrollContainer VerticalExpand="True" HorizontalExpand="True">
|
||||
<GridContainer Name="ResultsGrid" Access="Public" MaxGridWidth="300"/>
|
||||
</ScrollContainer>
|
||||
</BoxContainer>
|
||||
</windows:ActionsWindow>
|
||||
@@ -0,0 +1,36 @@
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Actions.Windows;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class ActionsWindow : DefaultWindow
|
||||
{
|
||||
public MultiselectOptionButton<Filters> FilterButton { get; private set; }
|
||||
|
||||
public ActionsWindow()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
SearchContainer.AddChild(FilterButton = new MultiselectOptionButton<Filters>
|
||||
{
|
||||
Label = Loc.GetString("ui-actionmenu-filter-button")
|
||||
});
|
||||
|
||||
foreach (var filter in Enum.GetValues<Filters>())
|
||||
{
|
||||
FilterButton.AddItem(filter.ToString(), filter);
|
||||
}
|
||||
}
|
||||
|
||||
public enum Filters
|
||||
{
|
||||
Enabled,
|
||||
Item,
|
||||
Innate,
|
||||
Instant,
|
||||
Targeted
|
||||
}
|
||||
}
|
||||
114
Content.Client/UserInterface/Systems/Admin/AdminUIController.cs
Normal file
114
Content.Client/UserInterface/Systems/Admin/AdminUIController.cs
Normal file
@@ -0,0 +1,114 @@
|
||||
using Content.Client.Administration.Managers;
|
||||
using Content.Client.Administration.UI;
|
||||
using Content.Client.Administration.UI.Tabs.PlayerTab;
|
||||
using Content.Client.Gameplay;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Client.Verbs;
|
||||
using Content.Shared.Input;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Console;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controllers;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Input.Binding;
|
||||
using Robust.Shared.Utility;
|
||||
using static Robust.Client.UserInterface.Controls.BaseButton;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Admin;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class AdminUIController : UIController, IOnStateEntered<GameplayState>, IOnStateExited<GameplayState>
|
||||
{
|
||||
[Dependency] private readonly IClientAdminManager _admin = default!;
|
||||
[Dependency] private readonly IClientConGroupController _conGroups = default!;
|
||||
[Dependency] private readonly IClientConsoleHost _conHost = default!;
|
||||
[Dependency] private readonly IInputManager _input = default!;
|
||||
|
||||
[UISystemDependency] private readonly VerbSystem _verbs = default!;
|
||||
|
||||
private AdminMenuWindow? _window;
|
||||
private MenuButton? _adminButton;
|
||||
|
||||
public void OnStateEntered(GameplayState state)
|
||||
{
|
||||
DebugTools.Assert(_window == null);
|
||||
_window = UIManager.CreateWindow<AdminMenuWindow>();
|
||||
_adminButton = UIManager.GetActiveUIWidget<MenuBar.Widgets.GameTopMenuBar>().AdminButton;
|
||||
LayoutContainer.SetAnchorPreset(_window, LayoutContainer.LayoutPreset.Center);
|
||||
_window.PlayerTabControl.OnEntryPressed += PlayerTabEntryPressed;
|
||||
_window.OnOpen += () => _adminButton.Pressed = true;
|
||||
_window.OnClose += () => _adminButton.Pressed = false;
|
||||
|
||||
_admin.AdminStatusUpdated += AdminStatusUpdated;
|
||||
|
||||
_adminButton.OnPressed += AdminButtonPressed;
|
||||
|
||||
_input.SetInputCommand(ContentKeyFunctions.OpenAdminMenu,
|
||||
InputCmdHandler.FromDelegate(_ => Toggle()));
|
||||
|
||||
AdminStatusUpdated();
|
||||
}
|
||||
|
||||
public void OnStateExited(GameplayState state)
|
||||
{
|
||||
if (_window != null)
|
||||
{
|
||||
_window.Dispose();
|
||||
_window = null;
|
||||
}
|
||||
|
||||
_admin.AdminStatusUpdated -= AdminStatusUpdated;
|
||||
|
||||
if (_adminButton != null)
|
||||
{
|
||||
_adminButton.Pressed = false;
|
||||
_adminButton.OnPressed -= AdminButtonPressed;
|
||||
_adminButton = null;
|
||||
}
|
||||
|
||||
CommandBinds.Unregister<AdminUIController>();
|
||||
}
|
||||
|
||||
private void AdminStatusUpdated()
|
||||
{
|
||||
_adminButton!.Visible = _conGroups.CanAdminMenu();
|
||||
}
|
||||
|
||||
private void AdminButtonPressed(ButtonEventArgs args)
|
||||
{
|
||||
Toggle();
|
||||
}
|
||||
|
||||
private void Toggle()
|
||||
{
|
||||
if (_window is {IsOpen: true})
|
||||
{
|
||||
_window.Close();
|
||||
}
|
||||
else if (_conGroups.CanAdminMenu())
|
||||
{
|
||||
_window?.Open();
|
||||
}
|
||||
}
|
||||
|
||||
private void PlayerTabEntryPressed(ButtonEventArgs args)
|
||||
{
|
||||
if (args.Button is not PlayerTabEntry button
|
||||
|| button.PlayerUid == null)
|
||||
return;
|
||||
|
||||
var uid = button.PlayerUid.Value;
|
||||
var function = args.Event.Function;
|
||||
|
||||
if (function == EngineKeyFunctions.UIClick)
|
||||
_conHost.ExecuteCommand($"vv {uid}");
|
||||
else if (function == EngineKeyFunctions.UseSecondary)
|
||||
_verbs.VerbMenu.OpenVerbMenu(uid, true);
|
||||
else
|
||||
return;
|
||||
|
||||
args.Event.Handle();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using Content.Client.Alerts;
|
||||
using Content.Client.Gameplay;
|
||||
using Content.Client.UserInterface.Systems.Alerts.Widgets;
|
||||
using Content.Shared.Alert;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controllers;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Alerts;
|
||||
|
||||
public sealed class AlertsUIController : UIController, IOnStateEntered<GameplayState>, IOnSystemChanged<ClientAlertsSystem>
|
||||
{
|
||||
[UISystemDependency] private readonly ClientAlertsSystem? _alertsSystem = default;
|
||||
|
||||
private AlertsUI? UI => UIManager.GetActiveUIWidgetOrNull<AlertsUI>();
|
||||
|
||||
private void OnAlertPressed(object? sender, AlertType e)
|
||||
{
|
||||
_alertsSystem?.AlertClicked(e);
|
||||
}
|
||||
|
||||
private void SystemOnClearAlerts(object? sender, EventArgs e)
|
||||
{
|
||||
UI?.ClearAllControls();
|
||||
}
|
||||
|
||||
private void SystemOnSyncAlerts(object? sender, IReadOnlyDictionary<AlertKey, AlertState> e)
|
||||
{
|
||||
if (sender is ClientAlertsSystem system)
|
||||
UI?.SyncControls(system, system.AlertOrder, e);
|
||||
}
|
||||
|
||||
public void OnSystemLoaded(ClientAlertsSystem system)
|
||||
{
|
||||
system.SyncAlerts += SystemOnSyncAlerts;
|
||||
system.ClearAlerts += SystemOnClearAlerts;
|
||||
}
|
||||
|
||||
public void OnSystemUnloaded(ClientAlertsSystem system)
|
||||
{
|
||||
system.SyncAlerts -= SystemOnSyncAlerts;
|
||||
system.ClearAlerts -= SystemOnClearAlerts;
|
||||
}
|
||||
|
||||
public void OnStateEntered(GameplayState state)
|
||||
{
|
||||
if (UI != null)
|
||||
{
|
||||
UI.AlertPressed += OnAlertPressed;
|
||||
}
|
||||
|
||||
// initially populate the frame if system is available
|
||||
var alerts = _alertsSystem?.ActiveAlerts;
|
||||
if (alerts != null)
|
||||
{
|
||||
SystemOnSyncAlerts(_alertsSystem, alerts);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,11 @@
|
||||
using System;
|
||||
using Content.Client.Actions.UI;
|
||||
using Content.Client.Actions.UI;
|
||||
using Content.Client.Cooldown;
|
||||
using Content.Shared.Alert;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.Alerts.UI
|
||||
namespace Content.Client.UserInterface.Systems.Alerts.Controls
|
||||
{
|
||||
public sealed class AlertControl : BaseButton
|
||||
{
|
||||
@@ -93,14 +90,7 @@ namespace Content.Client.Alerts.UI
|
||||
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;
|
||||
_cooldownGraphic.FromTime(Cooldown.Value.Start, Cooldown.Value.End);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<widgets:AlertsUI xmlns="https://spacestation14.io"
|
||||
xmlns:widgets="clr-namespace:Content.Client.UserInterface.Systems.Alerts.Widgets"
|
||||
MinSize="64 64">
|
||||
<PanelContainer HorizontalAlignment="Right" VerticalAlignment="Top">
|
||||
<BoxContainer Name="AlertContainer" Access="Public" Orientation="Vertical" />
|
||||
</PanelContainer>
|
||||
</widgets:AlertsUI>
|
||||
@@ -0,0 +1,154 @@
|
||||
using Content.Client.UserInterface.Systems.Alerts.Controls;
|
||||
using Content.Shared.Alert;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Input;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Alerts.Widgets;
|
||||
|
||||
/// <summary>
|
||||
/// The status effects display on the right side of the screen.
|
||||
/// </summary>
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class AlertsUI : UIWidget
|
||||
{
|
||||
// also known as Control.Children?
|
||||
private readonly Dictionary<AlertKey, AlertControl> _alertControls = new();
|
||||
|
||||
public AlertsUI()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public void SyncControls(AlertsSystem alertsSystem, AlertOrderPrototype? alertOrderPrototype,
|
||||
IReadOnlyDictionary<AlertKey, AlertState> alertStates)
|
||||
{
|
||||
// remove any controls with keys no longer present
|
||||
if (SyncRemoveControls(alertStates))
|
||||
return;
|
||||
|
||||
// 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
|
||||
SyncUpdateControls(alertsSystem, alertOrderPrototype, alertStates);
|
||||
}
|
||||
|
||||
public void ClearAllControls()
|
||||
{
|
||||
foreach (var alertControl in _alertControls.Values)
|
||||
{
|
||||
alertControl.OnPressed -= AlertControlPressed;
|
||||
alertControl.Dispose();
|
||||
}
|
||||
|
||||
_alertControls.Clear();
|
||||
}
|
||||
|
||||
public event EventHandler<AlertType>? AlertPressed;
|
||||
|
||||
private bool SyncRemoveControls(IReadOnlyDictionary<AlertKey, AlertState> alertStates)
|
||||
{
|
||||
var toRemove = new List<AlertKey>();
|
||||
foreach (var existingKey in _alertControls.Keys)
|
||||
{
|
||||
if (!alertStates.ContainsKey(existingKey))
|
||||
toRemove.Add(existingKey);
|
||||
}
|
||||
|
||||
foreach (var alertKeyToRemove in toRemove)
|
||||
{
|
||||
_alertControls.Remove(alertKeyToRemove, out var control);
|
||||
if (control == null)
|
||||
return true;
|
||||
AlertContainer.Children.Remove(control);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void SyncUpdateControls(AlertsSystem alertsSystem, AlertOrderPrototype? alertOrderPrototype,
|
||||
IReadOnlyDictionary<AlertKey, AlertState> alertStates)
|
||||
{
|
||||
foreach (var (alertKey, alertState) in alertStates)
|
||||
{
|
||||
if (!alertKey.AlertType.HasValue)
|
||||
{
|
||||
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 (!alertsSystem.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)
|
||||
{
|
||||
// key is the same, simply update the existing control severity / cooldown
|
||||
existingAlertControl.SetSeverity(alertState.Severity);
|
||||
existingAlertControl.Cooldown = alertState.Cooldown;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (existingAlertControl != null) AlertContainer.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, alertState);
|
||||
|
||||
//TODO: Can the presenter sort the states before giving it to us?
|
||||
if (alertOrderPrototype != null)
|
||||
{
|
||||
var added = false;
|
||||
foreach (var alertControl in AlertContainer.Children)
|
||||
{
|
||||
if (alertOrderPrototype.Compare(newAlert, ((AlertControl) alertControl).Alert) >= 0)
|
||||
continue;
|
||||
|
||||
var idx = alertControl.GetPositionInParent();
|
||||
AlertContainer.Children.Add(newAlertControl);
|
||||
newAlertControl.SetPositionInParent(idx);
|
||||
added = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!added)
|
||||
AlertContainer.Children.Add(newAlertControl);
|
||||
}
|
||||
else
|
||||
{
|
||||
AlertContainer.Children.Add(newAlertControl);
|
||||
}
|
||||
|
||||
_alertControls[newAlert.AlertKey] = newAlertControl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private AlertControl CreateAlertControl(AlertPrototype alert, AlertState alertState)
|
||||
{
|
||||
var alertControl = new AlertControl(alert, alertState.Severity)
|
||||
{
|
||||
Cooldown = alertState.Cooldown
|
||||
};
|
||||
alertControl.OnPressed += AlertControlPressed;
|
||||
return alertControl;
|
||||
}
|
||||
|
||||
private void AlertControlPressed(BaseButton.ButtonEventArgs args)
|
||||
{
|
||||
if (args.Button is not AlertControl control)
|
||||
return;
|
||||
|
||||
if (args.Event.Function != EngineKeyFunctions.UIClick)
|
||||
return;
|
||||
|
||||
AlertPressed?.Invoke(this, control.Alert.AlertType);
|
||||
}
|
||||
}
|
||||
323
Content.Client/UserInterface/Systems/Bwoink/AHelpUIController.cs
Normal file
323
Content.Client/UserInterface/Systems/Bwoink/AHelpUIController.cs
Normal file
@@ -0,0 +1,323 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Client.Administration.Managers;
|
||||
using Content.Client.Administration.Systems;
|
||||
using Content.Client.Administration.UI;
|
||||
using Content.Client.Administration.UI.CustomControls;
|
||||
using Content.Client.Gameplay;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Client.UserInterface.Systems.Info;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Input;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.UserInterface.Controllers;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Input.Binding;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Bwoink;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class AHelpUIController: UIController, IOnStateChanged<GameplayState>, IOnSystemChanged<BwoinkSystem>
|
||||
{
|
||||
[Dependency] private readonly IClientAdminManager _adminManager = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IClyde _clyde = default!;
|
||||
private BwoinkSystem? _bwoinkSystem;
|
||||
private MenuButton? _ahelpButton;
|
||||
private IAHelpUIHandler? _uiHelper;
|
||||
|
||||
public void OnStateEntered(GameplayState state)
|
||||
{
|
||||
DebugTools.Assert(_uiHelper == null);
|
||||
_ahelpButton = UIManager.GetActiveUIWidget<MenuBar.Widgets.GameTopMenuBar>().AHelpButton;
|
||||
_ahelpButton.OnPressed += AHelpButtonPressed;
|
||||
_adminManager.AdminStatusUpdated += OnAdminStatusUpdated;
|
||||
|
||||
CommandBinds.Builder
|
||||
.Bind(ContentKeyFunctions.OpenAHelp,
|
||||
InputCmdHandler.FromDelegate(_ => ToggleWindow()))
|
||||
.Register<AHelpUIController>();
|
||||
}
|
||||
|
||||
private void OnAdminStatusUpdated()
|
||||
{
|
||||
if (_uiHelper is not { IsOpen: true })
|
||||
return;
|
||||
EnsureUIHelper();
|
||||
}
|
||||
|
||||
private void AHelpButtonPressed(BaseButton.ButtonEventArgs obj)
|
||||
{
|
||||
EnsureUIHelper();
|
||||
_uiHelper!.ToggleWindow();
|
||||
}
|
||||
|
||||
public void OnStateExited(GameplayState state)
|
||||
{
|
||||
_uiHelper?.Dispose();
|
||||
_uiHelper = null;
|
||||
CommandBinds.Unregister<AHelpUIController>();
|
||||
}
|
||||
public void OnSystemLoaded(BwoinkSystem system)
|
||||
{
|
||||
_bwoinkSystem = system;
|
||||
_bwoinkSystem.OnBwoinkTextMessageRecieved += RecievedBwoink;
|
||||
}
|
||||
|
||||
public void OnSystemUnloaded(BwoinkSystem system)
|
||||
{
|
||||
_bwoinkSystem = null;
|
||||
}
|
||||
|
||||
private void SetAHelpPressed(bool pressed)
|
||||
{
|
||||
if (_ahelpButton == null || _ahelpButton.Pressed == pressed)
|
||||
return;
|
||||
_ahelpButton.StyleClasses.Remove(MenuButton.StyleClassRedTopButton);
|
||||
_ahelpButton.Pressed = pressed;
|
||||
}
|
||||
|
||||
private void RecievedBwoink(object? sender, SharedBwoinkSystem.BwoinkTextMessage message)
|
||||
{
|
||||
Logger.InfoS("c.s.go.es.bwoink", $"@{message.UserId}: {message.Text}");
|
||||
var localPlayer = _playerManager.LocalPlayer;
|
||||
if (localPlayer == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (localPlayer.UserId != message.TrueSender)
|
||||
{
|
||||
SoundSystem.Play("/Audio/Effects/adminhelp.ogg", Filter.Local());
|
||||
_clyde.RequestWindowAttention();
|
||||
}
|
||||
|
||||
EnsureUIHelper();
|
||||
if (!_uiHelper!.IsOpen)
|
||||
{
|
||||
_ahelpButton?.StyleClasses.Add(MenuButton.StyleClassRedTopButton);
|
||||
}
|
||||
_uiHelper!.Receive(message);
|
||||
}
|
||||
|
||||
public void EnsureUIHelper()
|
||||
{
|
||||
var isAdmin = _adminManager.HasFlag(AdminFlags.Adminhelp);
|
||||
|
||||
if (_uiHelper != null && _uiHelper.IsAdmin == isAdmin)
|
||||
return;
|
||||
|
||||
_uiHelper?.Dispose();
|
||||
var ownerUserId = _playerManager!.LocalPlayer!.UserId;
|
||||
_uiHelper = isAdmin ? new AdminAHelpUIHandler(ownerUserId) : new UserAHelpUIHandler(ownerUserId);
|
||||
|
||||
_uiHelper.SendMessageAction = (userId, textMessage) => _bwoinkSystem?.Send(userId, textMessage);
|
||||
_uiHelper.OnClose += () => { SetAHelpPressed(false); };
|
||||
_uiHelper.OnOpen += () => { SetAHelpPressed(true); };
|
||||
SetAHelpPressed(_uiHelper.IsOpen);
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
_uiHelper?.Close();
|
||||
}
|
||||
|
||||
public void Open()
|
||||
{
|
||||
var localPlayer = _playerManager.LocalPlayer;
|
||||
if (localPlayer == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
EnsureUIHelper();
|
||||
if (_uiHelper!.IsOpen)
|
||||
return;
|
||||
_uiHelper!.Open(localPlayer.UserId);
|
||||
}
|
||||
public void Open(NetUserId userId)
|
||||
{
|
||||
EnsureUIHelper();
|
||||
if (!_uiHelper!.IsAdmin)
|
||||
return;
|
||||
_uiHelper?.Open(userId);
|
||||
}
|
||||
public void ToggleWindow()
|
||||
{
|
||||
EnsureUIHelper();
|
||||
_uiHelper?.ToggleWindow();
|
||||
}
|
||||
}
|
||||
|
||||
public interface IAHelpUIHandler: IDisposable
|
||||
{
|
||||
public bool IsAdmin { get; }
|
||||
public bool IsOpen { get; }
|
||||
public void Receive(SharedBwoinkSystem.BwoinkTextMessage message);
|
||||
public void Close();
|
||||
public void Open(NetUserId netUserId);
|
||||
public void ToggleWindow();
|
||||
public event Action OnClose;
|
||||
public event Action OnOpen;
|
||||
public Action<NetUserId, string>? SendMessageAction { get; set; }
|
||||
}
|
||||
public sealed class AdminAHelpUIHandler : IAHelpUIHandler
|
||||
{
|
||||
private readonly NetUserId _ownerId;
|
||||
public AdminAHelpUIHandler(NetUserId owner)
|
||||
{
|
||||
_ownerId = owner;
|
||||
}
|
||||
private readonly Dictionary<NetUserId, BwoinkPanel> _activePanelMap = new();
|
||||
public bool IsAdmin => true;
|
||||
public bool IsOpen => _window is { Disposed: false, IsOpen: true };
|
||||
private BwoinkWindow? _window;
|
||||
|
||||
public void Receive(SharedBwoinkSystem.BwoinkTextMessage message)
|
||||
{
|
||||
var window = EnsurePanel(message.UserId);
|
||||
window.ReceiveLine(message);
|
||||
_window?.OnBwoink(message.UserId);
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
_window?.Close();
|
||||
}
|
||||
|
||||
public void ToggleWindow()
|
||||
{
|
||||
EnsurePanel(_ownerId);
|
||||
if (_window!.IsOpen)
|
||||
{
|
||||
_window.Close();
|
||||
}
|
||||
else
|
||||
{
|
||||
_window.OpenCentered();
|
||||
}
|
||||
}
|
||||
|
||||
public event Action? OnClose;
|
||||
public event Action? OnOpen;
|
||||
public Action<NetUserId, string>? SendMessageAction { get; set; }
|
||||
|
||||
public void Open(NetUserId channelId)
|
||||
{
|
||||
SelectChannel(channelId);
|
||||
_window?.OpenCentered();
|
||||
}
|
||||
|
||||
private void EnsureWindow()
|
||||
{
|
||||
if (_window is { Disposed: false })
|
||||
return;
|
||||
_window = new BwoinkWindow(this);
|
||||
_window.OnClose += () => { OnClose?.Invoke(); };
|
||||
_window.OnOpen += () => { OnOpen?.Invoke(); };
|
||||
}
|
||||
public BwoinkPanel EnsurePanel(NetUserId channelId)
|
||||
{
|
||||
EnsureWindow();
|
||||
|
||||
if (_activePanelMap.TryGetValue(channelId, out var existingPanel))
|
||||
return existingPanel;
|
||||
|
||||
_activePanelMap[channelId] = existingPanel = new BwoinkPanel(text => SendMessageAction?.Invoke(channelId, text));
|
||||
existingPanel.Visible = false;
|
||||
if (!_window!.BwoinkArea.Children.Contains(existingPanel))
|
||||
_window.BwoinkArea.AddChild(existingPanel);
|
||||
|
||||
return existingPanel;
|
||||
}
|
||||
public bool TryGetChannel(NetUserId ch, [NotNullWhen(true)] out BwoinkPanel? bp) => _activePanelMap.TryGetValue(ch, out bp);
|
||||
|
||||
private void SelectChannel(NetUserId uid)
|
||||
{
|
||||
EnsurePanel(uid);
|
||||
_window!.SelectChannel(uid);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_window?.Dispose();
|
||||
_window = null;
|
||||
_activePanelMap.Clear();
|
||||
}
|
||||
}
|
||||
public sealed class UserAHelpUIHandler : IAHelpUIHandler
|
||||
{
|
||||
private readonly NetUserId _ownerId;
|
||||
public UserAHelpUIHandler(NetUserId owner)
|
||||
{
|
||||
_ownerId = owner;
|
||||
}
|
||||
public bool IsAdmin => false;
|
||||
public bool IsOpen => _window is { Disposed: false, IsOpen: true };
|
||||
private DefaultWindow? _window;
|
||||
private BwoinkPanel? _chatPanel;
|
||||
|
||||
public void Receive(SharedBwoinkSystem.BwoinkTextMessage message)
|
||||
{
|
||||
DebugTools.Assert(message.UserId == _ownerId);
|
||||
EnsureInit();
|
||||
_chatPanel!.ReceiveLine(message);
|
||||
_window!.OpenCentered();
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
_window?.Close();
|
||||
}
|
||||
|
||||
public void ToggleWindow()
|
||||
{
|
||||
EnsureInit();
|
||||
if (_window!.IsOpen)
|
||||
{
|
||||
_window.Close();
|
||||
}
|
||||
else
|
||||
{
|
||||
_window.OpenCentered();
|
||||
}
|
||||
}
|
||||
|
||||
public event Action? OnClose;
|
||||
public event Action? OnOpen;
|
||||
public Action<NetUserId, string>? SendMessageAction { get; set; }
|
||||
|
||||
public void Open(NetUserId channelId)
|
||||
{
|
||||
EnsureInit();
|
||||
_window!.OpenCentered();
|
||||
}
|
||||
|
||||
private void EnsureInit()
|
||||
{
|
||||
if (_window is { Disposed: false })
|
||||
return;
|
||||
_chatPanel = new BwoinkPanel(text => SendMessageAction?.Invoke(_ownerId, text));
|
||||
_window = new DefaultWindow()
|
||||
{
|
||||
TitleClass="windowTitleAlert",
|
||||
HeaderClass="windowHeaderAlert",
|
||||
Title=Loc.GetString("bwoink-user-title"),
|
||||
SetSize=(400, 200),
|
||||
};
|
||||
_window.OnClose += () => { OnClose?.Invoke(); };
|
||||
_window.OnOpen += () => { OnOpen?.Invoke(); };
|
||||
_window.Contents.AddChild(_chatPanel);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_window?.Dispose();
|
||||
_window = null;
|
||||
_chatPanel = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
using Content.Client.CharacterInfo;
|
||||
using Content.Client.Gameplay;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Client.UserInterface.Systems.Character.Controls;
|
||||
using Content.Client.UserInterface.Systems.Character.Windows;
|
||||
using Content.Client.UserInterface.Systems.Objectives.Controls;
|
||||
using Content.Shared.Input;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controllers;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.Input.Binding;
|
||||
using Robust.Shared.Utility;
|
||||
using static Content.Client.CharacterInfo.CharacterInfoSystem;
|
||||
using static Robust.Client.UserInterface.Controls.BaseButton;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Character;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class CharacterUIController : UIController, IOnStateEntered<GameplayState>, IOnStateExited<GameplayState>, IOnSystemChanged<CharacterInfoSystem>
|
||||
{
|
||||
[UISystemDependency] private readonly CharacterInfoSystem _characterInfo = default!;
|
||||
|
||||
private CharacterWindow? _window;
|
||||
private MenuButton? _characterButton;
|
||||
|
||||
public void OnStateEntered(GameplayState state)
|
||||
{
|
||||
DebugTools.Assert(_window == null);
|
||||
_characterButton = UIManager.GetActiveUIWidget<MenuBar.Widgets.GameTopMenuBar>().CharacterButton;
|
||||
_characterButton.OnPressed += CharacterButtonPressed;
|
||||
|
||||
_window = UIManager.CreateWindow<CharacterWindow>();
|
||||
LayoutContainer.SetAnchorPreset(_window, LayoutContainer.LayoutPreset.CenterTop);
|
||||
|
||||
_window.OnClose += () => { _characterButton.Pressed = false; };
|
||||
_window.OnOpen += () => { _characterButton.Pressed = true; };
|
||||
|
||||
CommandBinds.Builder
|
||||
.Bind(ContentKeyFunctions.OpenCharacterMenu,
|
||||
InputCmdHandler.FromDelegate(_ => ToggleWindow()))
|
||||
.Register<CharacterUIController>();
|
||||
}
|
||||
|
||||
public void OnStateExited(GameplayState state)
|
||||
{
|
||||
if (_window != null)
|
||||
{
|
||||
_window.Dispose();
|
||||
_window = null;
|
||||
}
|
||||
|
||||
if (_characterButton != null)
|
||||
{
|
||||
_characterButton.OnPressed -= CharacterButtonPressed;
|
||||
_characterButton.Pressed = false;
|
||||
_characterButton = null;
|
||||
}
|
||||
|
||||
CommandBinds.Unregister<CharacterUIController>();
|
||||
}
|
||||
|
||||
public void OnSystemLoaded(CharacterInfoSystem system)
|
||||
{
|
||||
system.OnCharacterUpdate += CharacterUpdated;
|
||||
system.OnCharacterDetached += CharacterDetached;
|
||||
}
|
||||
|
||||
public void OnSystemUnloaded(CharacterInfoSystem system)
|
||||
{
|
||||
system.OnCharacterUpdate -= CharacterUpdated;
|
||||
system.OnCharacterDetached -= CharacterDetached;
|
||||
}
|
||||
|
||||
private void CharacterUpdated(CharacterData data)
|
||||
{
|
||||
if (_window == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var (job, objectives, briefing, sprite, entityName) = data;
|
||||
|
||||
_window.SubText.Text = job;
|
||||
_window.Objectives.RemoveAllChildren();
|
||||
|
||||
foreach (var (groupId, conditions) in objectives)
|
||||
{
|
||||
var objectiveControl = new CharacterObjectiveControl
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Vertical,
|
||||
Modulate = Color.Gray
|
||||
};
|
||||
|
||||
objectiveControl.AddChild(new Label
|
||||
{
|
||||
Text = groupId,
|
||||
Modulate = Color.LightSkyBlue
|
||||
});
|
||||
|
||||
foreach (var condition in conditions)
|
||||
{
|
||||
var conditionControl = new ObjectiveConditionsControl();
|
||||
conditionControl.ProgressTexture.Texture = condition.SpriteSpecifier.Frame0();
|
||||
conditionControl.ProgressTexture.Progress = condition.Progress;
|
||||
|
||||
conditionControl.Title.Text = condition.Title;
|
||||
conditionControl.Description.Text = condition.Description;
|
||||
|
||||
objectiveControl.AddChild(conditionControl);
|
||||
}
|
||||
|
||||
var briefingControl = new ObjectiveBriefingControl();
|
||||
briefingControl.Label.Text = briefing;
|
||||
|
||||
objectiveControl.AddChild(briefingControl);
|
||||
_window.Objectives.AddChild(objectiveControl);
|
||||
}
|
||||
|
||||
_window.SpriteView.Sprite = sprite;
|
||||
_window.NameLabel.Text = entityName;
|
||||
}
|
||||
|
||||
private void CharacterDetached()
|
||||
{
|
||||
CloseWindow();
|
||||
}
|
||||
|
||||
private void CharacterButtonPressed(ButtonEventArgs args)
|
||||
{
|
||||
ToggleWindow();
|
||||
}
|
||||
|
||||
private void CloseWindow()
|
||||
{
|
||||
_window!.Close();
|
||||
}
|
||||
|
||||
private void ToggleWindow()
|
||||
{
|
||||
if (_window == null)
|
||||
return;
|
||||
if (_window.IsOpen)
|
||||
{
|
||||
CloseWindow();
|
||||
}
|
||||
else
|
||||
{
|
||||
_characterInfo.RequestCharacterInfo();
|
||||
_window.Open();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<controls:CharacterObjectiveControl
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:cc="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Systems.Character.Controls"
|
||||
Orientation="Vertical"
|
||||
Modulate="#808080">
|
||||
<Label Name="Group" Modulate="#87CEFA" Access="Public"/>
|
||||
</controls:CharacterObjectiveControl>
|
||||
@@ -0,0 +1,14 @@
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Character.Controls;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class CharacterObjectiveControl : BoxContainer
|
||||
{
|
||||
public CharacterObjectiveControl()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<windows:CharacterWindow
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:cc="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
xmlns:windows="clr-namespace:Content.Client.UserInterface.Systems.Character.Windows"
|
||||
Title="{Loc 'character-info-title'}"
|
||||
MinWidth="400"
|
||||
MinHeight="545">
|
||||
<ScrollContainer>
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<SpriteView OverrideDirection="South" Scale="2 2" Name="SpriteView" Access="Public"/>
|
||||
<BoxContainer Orientation="Vertical" VerticalAlignment="Top">
|
||||
<Label Name="NameLabel" Access="Public"/>
|
||||
<Label Name="SubText" VerticalAlignment="Top" StyleClasses="LabelSubText" Access="Public"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
<Label Text="{Loc 'character-info-objectives-label'}" HorizontalAlignment="Center"/>
|
||||
<BoxContainer Orientation="Vertical" Name="Objectives" Access="Public"/>
|
||||
<cc:Placeholder PlaceholderText="{Loc 'character-info-roles-antagonist-text'}"/>
|
||||
</BoxContainer>
|
||||
</ScrollContainer>
|
||||
</windows:CharacterWindow>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user