feat(ui): complete retail item drop branches

Give retained buttons a reusable item-drop seam, wire the toolbar backpack target, and port retail stack-merge legality, capacity clamping, wire dispatch, destination selection, and immediate shortcut rekey notice. Record the live ACE merge gate and keep split quantity under AP-101.

Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-11 09:46:32 +02:00
parent 3281e0acb4
commit dc1649c493
19 changed files with 479 additions and 28 deletions

View file

@ -56,6 +56,8 @@ public class InventoryControllerTests
private static InventoryController Bind(ImportedLayout layout, ClientObjectTable objects,
int? strength = 100, List<uint>? uses = null, List<uint>? closes = null,
List<(uint item, uint container, int placement)>? puts = null,
List<(uint source, uint target, uint amount)>? merges = null,
List<(uint source, uint target)>? mergeNotices = null,
string? ownerName = null,
Action? onClose = null,
SelectionState? selection = null)
@ -66,6 +68,8 @@ public class InventoryControllerTests
sendUse: uses is null ? null : g => uses.Add(g),
sendNoLongerViewing: closes is null ? null : g => closes.Add(g),
sendPutItemInContainer: puts is null ? null : (i, c, p) => puts.Add((i, c, p)),
sendStackableMerge: merges is null ? null : (s, t, a) => merges.Add((s, t, a)),
notifyMergeAttempt: mergeNotices is null ? null : (s, t) => mergeNotices.Add((s, t)),
onClose: onClose,
selection: selection ?? new SelectionState());
@ -564,6 +568,67 @@ public class InventoryControllerTests
Assert.Contains((0xFFFFu, Player, 1), puts); // placement = occupied count (1) = first empty
}
[Fact]
public void Drop_matchingStackOnOccupiedStack_mergesBeforeMoveAndBroadcastsToolbarNotice()
{
var (layout, grid, _, _, _, _, _, _) = BuildLayout();
var objects = new ClientObjectTable();
objects.AddOrUpdate(new ClientObject
{
ObjectId = 0xA,
WeenieClassId = 0x1234,
StackSize = 40,
StackSizeMax = 100,
});
objects.MoveItem(0xA, Player, 0);
objects.AddOrUpdate(new ClientObject
{
ObjectId = 0xB,
WeenieClassId = 0x1234,
StackSize = 95,
StackSizeMax = 100,
});
objects.MoveItem(0xB, Player, 1);
var puts = new List<(uint item, uint container, int placement)>();
var merges = new List<(uint source, uint target, uint amount)>();
var notices = new List<(uint source, uint target)>();
var selection = new SelectionState();
var ctrl = Bind(layout, objects, puts: puts, merges: merges,
mergeNotices: notices, selection: selection);
ctrl.HandleDropRelease(grid, grid.GetItem(1)!, Payload(0xAu));
Assert.Equal(new[] { (0xAu, 0xBu, 5u) }, merges);
Assert.Equal(new[] { (0xAu, 0xBu) }, notices);
Assert.Empty(puts);
Assert.Equal(0xBu, selection.SelectedObjectId);
}
[Fact]
public void Drop_differentStackType_fallsThroughToNormalInventoryInsert()
{
var (layout, grid, _, _, _, _, _, _) = BuildLayout();
var objects = new ClientObjectTable();
objects.AddOrUpdate(new ClientObject
{
ObjectId = 0xA, WeenieClassId = 0x1111, StackSize = 10, StackSizeMax = 100,
});
objects.MoveItem(0xA, Player, 0);
objects.AddOrUpdate(new ClientObject
{
ObjectId = 0xB, WeenieClassId = 0x2222, StackSize = 10, StackSizeMax = 100,
});
objects.MoveItem(0xB, Player, 1);
var puts = new List<(uint item, uint container, int placement)>();
var merges = new List<(uint source, uint target, uint amount)>();
var ctrl = Bind(layout, objects, puts: puts, merges: merges);
ctrl.HandleDropRelease(grid, grid.GetItem(1)!, Payload(0xAu));
Assert.Empty(merges);
Assert.Contains((0xAu, Player, 1), puts);
}
[Fact]
public void Drop_onSideBagCell_movesIntoThatBag()
{

View file

@ -207,6 +207,59 @@ public class ToolbarControllerTests
Assert.False(interaction.IsTargetModeActive);
}
[Fact]
public void InventoryButton_freshItemDrop_acceptsAndPlacesInPlayerBackpack()
{
const uint player = 0x5000u;
const uint item = 0x5001u;
var (layout, _, _) = FakeToolbar();
var repo = new ClientObjectTable();
repo.AddOrUpdate(new ClientObject { ObjectId = player, Type = ItemType.Creature });
repo.AddOrUpdate(new ClientObject { ObjectId = item, Type = ItemType.Misc });
var puts = new List<(uint Item, uint Container, int Placement)>();
ToolbarController.Bind(layout, repo,
() => Array.Empty<ShortcutEntry>(),
iconIds: (_, _, _, _, _) => 0u,
useItem: _ => { },
playerGuid: () => player,
sendPutItemInContainer: (i, c, p) => puts.Add((i, c, p)));
var button = (UiButton)layout.FindElement(InventoryButtonId)!;
var payload = new ItemDragPayload(item, ItemDragSource.Inventory, 12, new UiItemSlot());
Assert.True(button.OnEvent(new UiEvent(0u, button, UiEventType.DragEnter, Payload: payload)));
Assert.Equal(ItemDragAcceptance.Accept, button.ItemDragAcceptanceForTest);
Assert.Equal(0x060011F7u, button.ItemDragAcceptSprite);
Assert.True(button.OnEvent(new UiEvent(0u, button, UiEventType.DropReleased, Payload: payload)));
Assert.Equal(ItemDragAcceptance.None, button.ItemDragAcceptanceForTest);
Assert.Equal(new[] { (item, player, 0) }, puts);
}
[Fact]
public void InventoryButton_shortcutAliasDrop_staysNeutralAndDoesNotMovePhysicalItem()
{
const uint player = 0x5000u;
const uint item = 0x5001u;
var (layout, _, _) = FakeToolbar();
var repo = new ClientObjectTable();
repo.AddOrUpdate(new ClientObject { ObjectId = item, Type = ItemType.Misc });
var puts = new List<(uint Item, uint Container, int Placement)>();
ToolbarController.Bind(layout, repo,
() => Array.Empty<ShortcutEntry>(),
iconIds: (_, _, _, _, _) => 0u,
useItem: _ => { },
playerGuid: () => player,
sendPutItemInContainer: (i, c, p) => puts.Add((i, c, p)));
var button = (UiButton)layout.FindElement(InventoryButtonId)!;
var payload = new ItemDragPayload(item, ItemDragSource.ShortcutBar, 3, new UiItemSlot());
button.OnEvent(new UiEvent(0u, button, UiEventType.DragEnter, Payload: payload));
Assert.Equal(ItemDragAcceptance.None, button.ItemDragAcceptanceForTest);
button.OnEvent(new UiEvent(0u, button, UiEventType.DropReleased, Payload: payload));
Assert.Empty(puts);
}
[Fact]
public void WindowButtonState_highlightsWhenOpen()
{