Lower generic spatial inventory interactions
This commit is contained in:
parent
e73da9faf5
commit
3bb7e6ae7d
4 changed files with 326 additions and 1 deletions
|
|
@ -0,0 +1,82 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using GameCult.Mesh;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Eve.Surface
|
||||
{
|
||||
public static class EveInventoryInteraction
|
||||
{
|
||||
public const string GridKind = "inventory.grid";
|
||||
public const string ItemKind = "inventory.item";
|
||||
public const string DragSessionKind = "inventory.drag_session";
|
||||
|
||||
public static bool TryCreateDropRequest(
|
||||
EveSurfaceDocument document,
|
||||
EveSurfaceComponent source,
|
||||
EveSurfaceComponent target,
|
||||
int destinationX,
|
||||
int destinationY,
|
||||
string clientId,
|
||||
out EveSurfaceCommandRequest? request)
|
||||
{
|
||||
request = null;
|
||||
if (document == null || source == null || target == null ||
|
||||
!string.Equals(source.Kind, ItemKind, StringComparison.Ordinal) ||
|
||||
!string.Equals(target.Kind, GridKind, StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var sourceKind = source.GetProp("sourceKind", source.GetProp("source"));
|
||||
var command = target.GetProp($"dropCommand.{sourceKind}", target.GetProp("dropCommand"));
|
||||
if (string.IsNullOrWhiteSpace(sourceKind) || string.IsNullOrWhiteSpace(command))
|
||||
return false;
|
||||
|
||||
var sourceIndex = Int(source.GetProp("sourceIndex"), -1);
|
||||
var targetIndex = Int(target.GetProp("targetIndex"), -1);
|
||||
var targetKind = target.GetProp("targetKind");
|
||||
var payload = new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["sourceKind"] = sourceKind,
|
||||
["originEntityKey"] = source.GetProp("sourceEntityKey", source.GetProp("entityKey")),
|
||||
["originIndex"] = sourceIndex.ToString(CultureInfo.InvariantCulture),
|
||||
["originCargoIndex"] = (string.Equals(sourceKind, "cargo", StringComparison.Ordinal) ? sourceIndex : -1)
|
||||
.ToString(CultureInfo.InvariantCulture),
|
||||
["itemKey"] = source.GetProp("itemKey"),
|
||||
["quantity"] = Math.Max(1, Int(source.GetProp("quantity"), 1)).ToString(CultureInfo.InvariantCulture),
|
||||
["sourceX"] = Int(source.GetProp("x"), int.MinValue).ToString(CultureInfo.InvariantCulture),
|
||||
["sourceY"] = Int(source.GetProp("y"), int.MinValue).ToString(CultureInfo.InvariantCulture),
|
||||
["destinationKind"] = targetKind,
|
||||
["destinationEntityKey"] = target.GetProp("targetEntityKey", target.GetProp("entityKey")),
|
||||
["destinationIndex"] = targetIndex.ToString(CultureInfo.InvariantCulture),
|
||||
["destinationCargoIndex"] = (string.Equals(targetKind, "cargo", StringComparison.Ordinal) ? targetIndex : -1)
|
||||
.ToString(CultureInfo.InvariantCulture),
|
||||
["destinationX"] = destinationX.ToString(CultureInfo.InvariantCulture),
|
||||
["destinationY"] = destinationY.ToString(CultureInfo.InvariantCulture),
|
||||
["hasDestinationPosition"] = "true"
|
||||
};
|
||||
var template = document.Commands.FirstOrDefault(candidate =>
|
||||
string.Equals(candidate.Command, command, StringComparison.Ordinal));
|
||||
var operation = template == null
|
||||
? CultMesh.OperationInvocation(command)
|
||||
: CultMesh.OperationInvocation(template.Operation);
|
||||
request = new EveSurfaceCommandRequest(
|
||||
document.ProviderId,
|
||||
document.Surface.Id,
|
||||
operation,
|
||||
CultMesh.OperationPayload(payload),
|
||||
DateTimeOffset.UtcNow,
|
||||
string.IsNullOrWhiteSpace(clientId) ? "unity-uitoolkit" : clientId);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int Int(string value, int fallback) =>
|
||||
int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed)
|
||||
? parsed
|
||||
: fallback;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 17fd4173e6e44b4b9c503d343021ffcb
|
||||
|
|
@ -13,6 +13,10 @@ namespace GameCult.Eve.UnityUIToolkit
|
|||
public sealed class EveUiToolkitSurfaceLowerer
|
||||
{
|
||||
private readonly EveUiToolkitSurfaceOptions _options;
|
||||
private EveSurfaceComponent? _inventoryDragSource;
|
||||
private Vector2 _inventoryDragStart;
|
||||
private bool _inventoryPointerMoved;
|
||||
private bool _suppressInventoryClick;
|
||||
|
||||
public EveUiToolkitSurfaceLowerer(EveUiToolkitSurfaceOptions? options = null)
|
||||
{
|
||||
|
|
@ -43,7 +47,11 @@ namespace GameCult.Eve.UnityUIToolkit
|
|||
ApplyGeneratedLayout(element, component);
|
||||
|
||||
foreach (var child in component.Children)
|
||||
element.Add(LowerComponent(child, document, commandSink));
|
||||
{
|
||||
var loweredChild = LowerComponent(child, document, commandSink);
|
||||
PositionInventoryChild(component, child, loweredChild);
|
||||
element.Add(loweredChild);
|
||||
}
|
||||
|
||||
foreach (var slot in component.EmbeddedDocuments)
|
||||
{
|
||||
|
|
@ -97,6 +105,12 @@ namespace GameCult.Eve.UnityUIToolkit
|
|||
element.style.alignItems = Align.Stretch;
|
||||
return element;
|
||||
}
|
||||
case EveInventoryInteraction.GridKind:
|
||||
return InventoryGrid(component, document, commandSink);
|
||||
case EveInventoryInteraction.ItemKind:
|
||||
return InventoryItem(component, document, commandSink);
|
||||
case EveInventoryInteraction.DragSessionKind:
|
||||
return InventoryDragSession(component);
|
||||
case "partition":
|
||||
{
|
||||
var element = new VisualElement();
|
||||
|
|
@ -210,6 +224,167 @@ namespace GameCult.Eve.UnityUIToolkit
|
|||
}
|
||||
}
|
||||
|
||||
private VisualElement InventoryGrid(
|
||||
EveSurfaceComponent component,
|
||||
EveSurfaceDocument document,
|
||||
Action<EveSurfaceCommandRequest>? commandSink)
|
||||
{
|
||||
var grid = new VisualElement();
|
||||
grid.AddToClassList("eve-inventory-grid");
|
||||
grid.style.position = Position.Relative;
|
||||
var columns = Math.Max(1, ParseInt(component.GetProp("columns"), 6));
|
||||
var rows = Math.Max(1, ParseInt(component.GetProp("rows"), 3));
|
||||
var cellSize = Math.Max(1f, ParseNumber(component.GetProp("cellSize"), 72f));
|
||||
var gap = Math.Max(0f, ParseNumber(component.GetProp("cellGap"), 4f));
|
||||
grid.style.width = columns * cellSize + Math.Max(0, columns - 1) * gap;
|
||||
grid.style.height = rows * cellSize + Math.Max(0, rows - 1) * gap;
|
||||
for (var index = 0; index < Math.Min(columns * rows, 256); index++)
|
||||
{
|
||||
var cell = new VisualElement();
|
||||
cell.AddToClassList("eve-inventory-cell");
|
||||
cell.style.position = Position.Absolute;
|
||||
cell.style.left = (index % columns) * (cellSize + gap);
|
||||
cell.style.top = (index / columns) * (cellSize + gap);
|
||||
cell.style.width = cellSize;
|
||||
cell.style.height = cellSize;
|
||||
grid.Add(cell);
|
||||
}
|
||||
grid.RegisterCallback<ClickEvent>(evt =>
|
||||
{
|
||||
if (_inventoryDragSource == null || _suppressInventoryClick)
|
||||
return;
|
||||
if (TryEmitInventoryDrop(document, _inventoryDragSource, component, grid, evt.position, commandSink))
|
||||
_inventoryDragSource = null;
|
||||
});
|
||||
return grid;
|
||||
}
|
||||
|
||||
private VisualElement InventoryItem(
|
||||
EveSurfaceComponent component,
|
||||
EveSurfaceDocument document,
|
||||
Action<EveSurfaceCommandRequest>? commandSink)
|
||||
{
|
||||
var item = new Button { text = component.GetProp("label", component.GetProp("itemKey")) };
|
||||
item.AddToClassList("eve-inventory-item");
|
||||
item.RegisterCallback<PointerDownEvent>(evt =>
|
||||
{
|
||||
if (!ParseBool(component.GetProp("draggable", "true")))
|
||||
return;
|
||||
_inventoryDragSource = component;
|
||||
_inventoryDragStart = evt.position;
|
||||
_inventoryPointerMoved = false;
|
||||
item.CapturePointer(evt.pointerId);
|
||||
evt.StopPropagation();
|
||||
});
|
||||
item.RegisterCallback<PointerMoveEvent>(evt =>
|
||||
{
|
||||
if (_inventoryDragSource != component)
|
||||
return;
|
||||
var pointerPosition = new Vector2(evt.position.x, evt.position.y);
|
||||
if ((pointerPosition - _inventoryDragStart).sqrMagnitude > 16f)
|
||||
_inventoryPointerMoved = true;
|
||||
});
|
||||
item.RegisterCallback<PointerUpEvent>(evt =>
|
||||
{
|
||||
if (_inventoryDragSource != component)
|
||||
return;
|
||||
item.ReleasePointer(evt.pointerId);
|
||||
if (_inventoryPointerMoved)
|
||||
{
|
||||
var picked = item.panel?.Pick(evt.position);
|
||||
var targetElement = InventoryGridAncestor(picked);
|
||||
if (targetElement?.userData is EveSurfaceComponent target)
|
||||
TryEmitInventoryDrop(document, component, target, targetElement, evt.position, commandSink);
|
||||
_inventoryDragSource = null;
|
||||
_suppressInventoryClick = true;
|
||||
}
|
||||
evt.StopPropagation();
|
||||
});
|
||||
item.RegisterCallback<ClickEvent>(evt =>
|
||||
{
|
||||
if (_suppressInventoryClick)
|
||||
{
|
||||
_suppressInventoryClick = false;
|
||||
evt.StopPropagation();
|
||||
return;
|
||||
}
|
||||
_inventoryDragSource = component;
|
||||
evt.StopPropagation();
|
||||
});
|
||||
return item;
|
||||
}
|
||||
|
||||
private static VisualElement InventoryDragSession(EveSurfaceComponent component)
|
||||
{
|
||||
var panel = new VisualElement();
|
||||
panel.AddToClassList("eve-inventory-drag-session");
|
||||
var active = ParseBool(component.GetProp("active"));
|
||||
panel.Add(BodyLabel(active
|
||||
? component.GetProp("itemKey", "Dragging")
|
||||
: "No active drag"));
|
||||
return panel;
|
||||
}
|
||||
|
||||
private static void PositionInventoryChild(
|
||||
EveSurfaceComponent parent,
|
||||
EveSurfaceComponent child,
|
||||
VisualElement element)
|
||||
{
|
||||
if (!string.Equals(parent.Kind, EveInventoryInteraction.GridKind, StringComparison.Ordinal) ||
|
||||
!string.Equals(child.Kind, EveInventoryInteraction.ItemKind, StringComparison.Ordinal))
|
||||
return;
|
||||
var cellSize = Math.Max(1f, ParseNumber(parent.GetProp("cellSize"), 72f));
|
||||
var gap = Math.Max(0f, ParseNumber(parent.GetProp("cellGap"), 4f));
|
||||
element.style.position = Position.Absolute;
|
||||
element.style.left = Math.Max(0, ParseInt(child.GetProp("x"), 0)) * (cellSize + gap);
|
||||
element.style.top = Math.Max(0, ParseInt(child.GetProp("y"), 0)) * (cellSize + gap);
|
||||
element.style.width = cellSize;
|
||||
element.style.height = cellSize;
|
||||
}
|
||||
|
||||
private static VisualElement? InventoryGridAncestor(VisualElement? element)
|
||||
{
|
||||
while (element != null)
|
||||
{
|
||||
if (element.userData is EveSurfaceComponent component &&
|
||||
string.Equals(component.Kind, EveInventoryInteraction.GridKind, StringComparison.Ordinal))
|
||||
return element;
|
||||
element = element.parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool TryEmitInventoryDrop(
|
||||
EveSurfaceDocument document,
|
||||
EveSurfaceComponent source,
|
||||
EveSurfaceComponent target,
|
||||
VisualElement targetElement,
|
||||
Vector2 panelPosition,
|
||||
Action<EveSurfaceCommandRequest>? commandSink)
|
||||
{
|
||||
if (commandSink == null)
|
||||
return false;
|
||||
var local = targetElement.WorldToLocal(panelPosition);
|
||||
var pitch = Math.Max(1f, ParseNumber(target.GetProp("cellSize"), 72f) +
|
||||
Math.Max(0f, ParseNumber(target.GetProp("cellGap"), 4f)));
|
||||
var x = Math.Max(0, (int)Math.Floor(local.x / pitch));
|
||||
var y = Math.Max(0, (int)Math.Floor(local.y / pitch));
|
||||
if (!EveInventoryInteraction.TryCreateDropRequest(
|
||||
document, source, target, x, y, "unity-uitoolkit", out var request) || request == null)
|
||||
return false;
|
||||
commandSink(request);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int ParseInt(string value, int fallback) =>
|
||||
int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed) ? parsed : fallback;
|
||||
|
||||
private static bool ParseBool(string value) =>
|
||||
bool.TryParse(value, out var parsed) && parsed;
|
||||
|
||||
private static float ParseNumber(string value, float fallback) =>
|
||||
float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed) ? parsed : fallback;
|
||||
|
||||
private static void EmitCommand(
|
||||
EveSurfaceDocument document,
|
||||
EveSurfaceComponent component,
|
||||
|
|
|
|||
|
|
@ -142,6 +142,72 @@ namespace GameCult.Eve.UnityUIToolkit.Tests
|
|||
Assert.That(root.style.bottom.value.value, Is.EqualTo(32f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void InventoryDropBuildsAdvertisedTypedOperationPayload()
|
||||
{
|
||||
var source = Component("ore", EveInventoryInteraction.ItemKind, new Dictionary<string, string>
|
||||
{
|
||||
["sourceKind"] = "cargo",
|
||||
["sourceEntityKey"] = "zone.0.entity.1",
|
||||
["sourceIndex"] = "2",
|
||||
["itemKey"] = "ore",
|
||||
["quantity"] = "4",
|
||||
["x"] = "3",
|
||||
["y"] = "5"
|
||||
});
|
||||
var target = Component("equipment", EveInventoryInteraction.GridKind, new Dictionary<string, string>
|
||||
{
|
||||
["targetKind"] = "equipment",
|
||||
["targetEntityKey"] = "zone.0.entity.1",
|
||||
["targetIndex"] = "-1",
|
||||
["dropCommand.cargo"] = "aetheria.daemon.commands.EquipItem"
|
||||
});
|
||||
|
||||
Assert.That(EveInventoryInteraction.TryCreateDropRequest(
|
||||
Document(target), source, target, 7, 9, "unity-test", out var request), Is.True);
|
||||
Assert.That(request, Is.Not.Null);
|
||||
Assert.That(request!.Command, Is.EqualTo("aetheria.daemon.commands.EquipItem"));
|
||||
Assert.That(request.Payload.GetString("originEntityKey"), Is.EqualTo("zone.0.entity.1"));
|
||||
Assert.That(request.Payload.GetString("originCargoIndex"), Is.EqualTo("2"));
|
||||
Assert.That(request.Payload.GetString("destinationX"), Is.EqualTo("7"));
|
||||
Assert.That(request.Payload.GetString("destinationY"), Is.EqualTo("9"));
|
||||
Assert.That(request.Payload.GetString("hasDestinationPosition"), Is.EqualTo("true"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void InventoryGridAndItemsLowerToNativeSpatialElements()
|
||||
{
|
||||
var item = Component("ore", EveInventoryInteraction.ItemKind, new Dictionary<string, string>
|
||||
{
|
||||
["label"] = "Iron Ore",
|
||||
["sourceKind"] = "cargo",
|
||||
["x"] = "1",
|
||||
["y"] = "2"
|
||||
});
|
||||
var grid = new EveSurfaceComponent(
|
||||
"cargo",
|
||||
EveInventoryInteraction.GridKind,
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
["columns"] = "4",
|
||||
["rows"] = "3",
|
||||
["cellSize"] = "32",
|
||||
["cellGap"] = "2"
|
||||
},
|
||||
new[] { item });
|
||||
|
||||
var root = new EveUiToolkitSurfaceLowerer().Lower(Document(grid));
|
||||
var loweredItem = root.Query<Button>(className: "eve-inventory-item").First();
|
||||
|
||||
Assert.That(root.ClassListContains("eve-inventory-grid"), Is.True);
|
||||
Assert.That(root.style.width.value.value, Is.EqualTo(134f));
|
||||
Assert.That(root.style.height.value.value, Is.EqualTo(100f));
|
||||
Assert.That(loweredItem, Is.Not.Null);
|
||||
Assert.That(loweredItem.text, Is.EqualTo("Iron Ore"));
|
||||
Assert.That(loweredItem.style.left.value.value, Is.EqualTo(34f));
|
||||
Assert.That(loweredItem.style.top.value.value, Is.EqualTo(68f));
|
||||
}
|
||||
|
||||
private static EveSurfaceDocument Document(EveSurfaceComponent root, string surfaceId = "test-surface")
|
||||
{
|
||||
return new EveSurfaceDocument(
|
||||
|
|
|
|||
Reference in a new issue