Skip to content

Commit

Permalink
First-pass implementation of drag+drop in inventory
Browse files Browse the repository at this point in the history
1. Single-click to start dragging
2. Click+hold to start dragging
3. Double-click to equip still works
4. Dragging one item over multiple will reset it to it's original spot (or continue dragging if it no longer fits)
5. Dragging one item over another will start dragging that other item
  • Loading branch information
ethanmoffat committed Mar 28, 2022
1 parent fa9898d commit e5b6c1b
Show file tree
Hide file tree
Showing 3 changed files with 261 additions and 38 deletions.
138 changes: 117 additions & 21 deletions EndlessClient/HUD/Inventory/InventoryPanelItem.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
using EndlessClient.HUD.Panels;
using EOLib;
using EOLib.Domain.Character;
using EOLib.Domain.Item;
using EOLib.Graphics;
using EOLib.IO.Extensions;
using EOLib.IO.Pub;
using EOLib.Localization;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
Expand All @@ -17,10 +15,21 @@ namespace EndlessClient.HUD.Inventory
{
public class InventoryPanelItem : XNAControl
{
private readonly IStatusLabelSetter _statusLabelSetter;
private readonly InventoryPanel _inventoryPanel;
private readonly EIFRecord _data;
public class ItemDragCompletedEventArgs
{
public bool ContinueDrag { get; set; } = false;

public bool RestoreOriginalSlot { get; set; } = false;

public EIFRecord Data { get; }

public ItemDragCompletedEventArgs(EIFRecord data) => Data = data;
}

// uses absolute coordinates
private static readonly Rectangle InventoryGridArea = new Rectangle(110, 334, 377, 116);

private readonly InventoryPanel _inventoryPanel;
private readonly Texture2D _itemGraphic;
private readonly Texture2D _highlightBackground;
private readonly XNALabel _nameLabel;
Expand All @@ -30,13 +39,26 @@ public class InventoryPanelItem : XNAControl
private readonly Stopwatch _clickTimer;
private int _recentClicks;

private ulong _updateTick;

// Ru Paul's drag properties
private bool _beingDragged;
private Vector2 _oldOffset;

private bool MousePressed => CurrentMouseState.LeftButton == ButtonState.Pressed && PreviousMouseState.LeftButton == ButtonState.Released;

private bool MouseReleased => CurrentMouseState.LeftButton == ButtonState.Released && PreviousMouseState.LeftButton == ButtonState.Pressed;

private bool MouseHeld => CurrentMouseState.LeftButton == ButtonState.Pressed && PreviousMouseState.LeftButton == ButtonState.Pressed;

public int Slot
{
get => _slot;
set
{
_slot = value;
DrawPosition = GetPosition(_slot);
DrawOrder = 102 - (_slot % InventoryPanel.InventoryRowSlots) * 2;
}
}

Expand All @@ -52,14 +74,19 @@ public string Text
}
}

public bool IsDragging => _beingDragged;

public EIFRecord Data { get; }

public event EventHandler<EIFRecord> DoubleClick;
public event EventHandler<ItemDragCompletedEventArgs> DoneDragging;

public InventoryPanelItem(InventoryPanel inventoryPanel, int slot, IInventoryItem inventoryItem, EIFRecord data)
{
_inventoryPanel = inventoryPanel;
Slot = slot;
InventoryItem = inventoryItem;
_data = data;
Data = data;

_itemGraphic = inventoryPanel.NativeGraphicsManager.TextureFromResource(GFXTypes.Items, 2 * data.Graphic, transparent: true);
_highlightBackground = new Texture2D(Game.GraphicsDevice, 1, 1);
Expand All @@ -75,15 +102,37 @@ public InventoryPanelItem(InventoryPanel inventoryPanel, int slot, IInventoryIte
Text = string.Empty
};

OnMouseEnter += (_, _) => _nameLabel.Visible = true;
OnMouseEnter += (_, _) => _nameLabel.Visible = !_beingDragged;
OnMouseLeave += (_, _) => _nameLabel.Visible = false;

var (slotWidth, slotHeight) = _data.Size.GetDimensions();
SetSize(slotWidth * 26, slotHeight * 26);
var (slotWidth, slotHeight) = Data.Size.GetDimensions();
SetSize(slotWidth * 26 - 3, slotHeight * 26 - 3);

_clickTimer = new Stopwatch();
}

public int GetCurrentSlotBasedOnPosition()
{
if (!_beingDragged)
return Slot;

return (int)((DrawPosition.X - _oldOffset.X) / 26) + InventoryPanel.InventoryRowSlots * (int)((DrawPosition.Y - _oldOffset.Y) / 26);
}

public void StartDragging()
{
_beingDragged = true;
_nameLabel.Visible = false;

_oldOffset = DrawPositionWithParentOffset - DrawPosition;

// todo: drag without unparenting this control
SetControlUnparented();
AddControlToDefaultGame();

DrawOrder = 1000;
}

public override void Initialize()
{
_nameLabel.Initialize();
Expand All @@ -95,23 +144,63 @@ public override void Initialize()

protected override void OnUpdateControl(GameTime gameTime)
{
if (_recentClicks > 0 && _clickTimer.Elapsed.TotalSeconds > 1)
if (_recentClicks > 0)
{
_clickTimer.Restart();
_recentClicks--;
if (_clickTimer.Elapsed.TotalMilliseconds > 500)
{
_clickTimer.Restart();
_recentClicks--;
}
else if (_clickTimer.Elapsed.TotalMilliseconds > 200 && _inventoryPanel.NoItemsDragging())
{
StartDragging();
}
}

if (MouseOver && MouseOverPreviously)
if (MousePressed)
_updateTick = 0;

if (!_beingDragged && MouseOver && MouseOverPreviously && MouseReleased)
{
if (CurrentMouseState.LeftButton == ButtonState.Released && PreviousMouseState.LeftButton == ButtonState.Pressed)
_clickTimer.Restart();
_recentClicks++;

if (_recentClicks == 2)
{
_clickTimer.Restart();
_recentClicks++;
DoubleClick?.Invoke(this, Data);
_recentClicks = 0;
}
}
else if (++_updateTick % 8 == 0 && !_beingDragged && MouseOver && MouseOverPreviously && MouseHeld)
{
if (_inventoryPanel.NoItemsDragging())
{
StartDragging();
}
}
else if (_beingDragged)
{
DrawPosition = new Vector2(CurrentMouseState.X - (DrawArea.Width / 2), CurrentMouseState.Y - (DrawArea.Height / 2));

if (MouseReleased)
{
var args = new ItemDragCompletedEventArgs(Data);
DoneDragging?.Invoke(this, args);

if (_recentClicks == 2)
if (!args.ContinueDrag)
{
DoubleClick?.Invoke(this, _data);
_recentClicks = 0;
if (Game.Components.Contains(this))
Game.Components.Remove(this);

SetParentControl(_inventoryPanel);

if (!args.RestoreOriginalSlot)
Slot = GetCurrentSlotBasedOnPosition();
else
DrawPosition = GetPosition(Slot);

_beingDragged = false;
_nameLabel.Visible = false;
}
}
}
Expand All @@ -125,10 +214,17 @@ protected override void OnDrawControl(GameTime gameTime)

if (MouseOver)
{
_spriteBatch.Draw(_highlightBackground, DrawAreaWithParentOffset.WithSize(DrawArea.Width - 3, DrawArea.Height - 3), Color.White);
// slot based on current mouse position if being dragged
var currentSlot = GetCurrentSlotBasedOnPosition();
var drawPosition = GetPosition(currentSlot) + (_beingDragged ? _oldOffset : ImmediateParent.DrawPositionWithParentOffset);

if (InventoryGridArea.Contains(drawPosition))
{
_spriteBatch.Draw(_highlightBackground, DrawArea.WithPosition(drawPosition), Color.White);
}
}

_spriteBatch.Draw(_itemGraphic, DrawPositionWithParentOffset, Color.White);
_spriteBatch.Draw(_itemGraphic, DrawPositionWithParentOffset, Color.FromNonPremultiplied(255, 255, 255, _beingDragged ? 128 : 255));

_spriteBatch.End();

Expand Down
32 changes: 28 additions & 4 deletions EndlessClient/HUD/Inventory/InventoryService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using EOLib.IO;
using EOLib.IO.Extensions;
using Optional;
using System.Collections.Generic;

namespace EndlessClient.HUD.Inventory
{
Expand All @@ -14,7 +15,7 @@ public Option<int> GetNextOpenSlot(bool[,] usedSlots, ItemSize size, Option<int>
var (sizeWidth, sizeHeight) = size.GetDimensions();

var preferredSlotIsValid = preferredSlot.Match(
some: slot => IsSlotOpen(usedSlots, slot, size),
some: slot => IsSlotOpen(usedSlots, Option.None<int>(), slot, size),
none: () => false);

if (preferredSlotIsValid)
Expand All @@ -25,7 +26,7 @@ public Option<int> GetNextOpenSlot(bool[,] usedSlots, ItemSize size, Option<int>
for (int c = 0; c < usedSlots.GetLength(1); c++)
{
var slot = r * InventoryPanel.InventoryRowSlots + c;
if (!usedSlots[r, c] && IsSlotOpen(usedSlots, slot, size))
if (!usedSlots[r, c] && IsSlotOpen(usedSlots, Option.None<int>(), slot, size))
return Option.Some(slot);
}
}
Expand All @@ -43,17 +44,36 @@ public void ClearSlots(bool[,] usedSlots, int slot, ItemSize size)
SetSlotValue(usedSlots, slot, size, value: false);
}

private bool IsSlotOpen(bool[,] usedSlots, int slot, ItemSize size)
public bool FitsInSlot(bool[,] usedSlots, int oldSlot, int newSlot, ItemSize size) =>
IsSlotOpen(usedSlots, Option.Some(oldSlot), newSlot, size);

public bool FitsInSlot(bool[,] usedSlots, int newSlot, ItemSize size) =>
IsSlotOpen(usedSlots, Option.None<int>(), newSlot, size);

private bool IsSlotOpen(bool[,] usedSlots, Option<int> oldSlot, int slot, ItemSize size)
{
var (sizeWidth, sizeHeight) = size.GetDimensions();

var ignorePoints = new List<(int X, int Y)>();
oldSlot.MatchSome(s =>
{
var oldCol = s % InventoryPanel.InventoryRowSlots;
var oldRow = s / InventoryPanel.InventoryRowSlots;

for (int r = oldRow; r < oldRow + sizeHeight; r++)
for (int c = oldCol; c < oldCol + sizeWidth; c++)
ignorePoints.Add((c, r));
});

var col = slot % InventoryPanel.InventoryRowSlots;
var row = slot / InventoryPanel.InventoryRowSlots;
for (int r = row; r < row + sizeHeight; r++)
{
for (int c = col; c < col + sizeWidth; c++)
{
if (r >= usedSlots.GetLength(0) || c >= usedSlots.GetLength(1) || usedSlots[r, c])
if (r >= usedSlots.GetLength(0) || c >= usedSlots.GetLength(1) ||
r < 0 || c < 0 ||
(!ignorePoints.Contains((c, r)) && usedSlots[r, c]))
return false;
}
}
Expand Down Expand Up @@ -85,5 +105,9 @@ public interface IInventoryService
void SetSlots(bool[,] usedSlots, int slot, ItemSize size);

void ClearSlots(bool[,] usedSlots, int slot, ItemSize size);

bool FitsInSlot(bool[,] usedSlots, int oldSlot, int newSlot, ItemSize size);

bool FitsInSlot(bool[,] usedSlots, int newSlot, ItemSize size);
}
}
Loading

0 comments on commit e5b6c1b

Please sign in to comment.