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 { /// /// A slot in the action hotbar. Not extending BaseButton because /// its needs diverged too much. /// 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"; /// /// Current action in this slot. /// public ActionType? Action { get; private set; } /// /// 1-10 corresponding to the number label on the slot (10 is labeled as 0) /// private byte SlotNumber => (byte) (SlotIndex + 1); public byte SlotIndex { get; } private readonly IGameTiming _gameTiming; private readonly RichTextLabel _number; private readonly TextureRect _bigActionIcon; private readonly TextureRect _smallActionIcon; private readonly SpriteView _smallItemSpriteView; private readonly SpriteView _bigItemSpriteView; private readonly CooldownGraphic _cooldownGraphic; private readonly ActionsUI _actionsUI; private readonly ActionMenu _actionMenu; // whether button is currently pressed down by mouse or keybind down. private bool _depressed; private bool _beingHovered; /// /// Creates an action slot for the specified number /// /// slot index this corresponds to, 0-9 (0 labeled as 1, 8, labeled "9", 9 labeled as "0". public ActionSlot(ActionsUI actionsUI, ActionMenu actionMenu, byte slotIndex) { _actionsUI = actionsUI; _actionMenu = actionMenu; _gameTiming = IoCManager.Resolve(); SlotIndex = slotIndex; MouseFilter = MouseFilterMode.Stop; MinSize = (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.Name)); 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(this, _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(); } /// /// Cancel current press without triggering the action /// public void CancelPress() { _depressed = false; DrawModeChanged(); } /// /// Press this button down. If it was depressed and now set to not depressed, will /// trigger the action. /// 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; } /// /// Updates the item action assigned to this slot, tied to a specific item. /// /// action to assign /// item the action is provided by public void Assign(ActionType action) { // already assigned if (Action != null && Action == action) return; Action = action; HideTooltip(); UpdateIcons(); DrawModeChanged(); _number.SetMessage(SlotNumberLabel()); } /// /// Clears the action assigned to this slot /// public void Clear() { if (Action == null) return; Action = null; _depressed = false; HideTooltip(); UpdateIcons(); DrawModeChanged(); _number.SetMessage(SlotNumberLabel()); } /// /// Display the action in this slot (if there is one) as enabled /// public void Enable() { DrawModeChanged(); _number.SetMessage(SlotNumberLabel()); } /// /// Display the action in this slot (if there is one) as disabled. /// The slot is still clickable. /// 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.Provider != 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?.Provider == null || !IoCManager.Resolve().TryGetComponent(Action.Provider.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 (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(); } } } }