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:
parent
3281e0acb4
commit
dc1649c493
19 changed files with 479 additions and 28 deletions
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
using System.Net;
|
||||
using AcDream.Core.Net.Messages;
|
||||
|
||||
namespace AcDream.Core.Net.Tests;
|
||||
|
||||
public sealed class WorldSessionInventoryActionTests
|
||||
{
|
||||
[Fact]
|
||||
public void SendStackableMerge_UsesNextSequenceAndExactBuilderBytes()
|
||||
{
|
||||
using var session = new WorldSession(
|
||||
new IPEndPoint(IPAddress.Loopback, 65000));
|
||||
byte[]? captured = null;
|
||||
session.GameActionCapture = body => captured = body;
|
||||
|
||||
session.SendStackableMerge(0xAAu, 0xBBu, 5u);
|
||||
|
||||
Assert.Equal(InventoryActions.BuildStackableMerge(1, 0xAAu, 0xBBu, 5u), captured);
|
||||
}
|
||||
}
|
||||
|
|
@ -121,11 +121,11 @@ public sealed class ShortcutDropPlannerTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void FullStackMerge_RekeysAllMatchesAndPreservesRawSpellWord()
|
||||
public void FullStackMerge_RemovesExistingTargetShortcutThenRekeysSourceSlot()
|
||||
{
|
||||
var slots = Empty();
|
||||
slots[2] = new ShortcutEntry(2, 0xA001u, 0x11223344u);
|
||||
slots[9] = new ShortcutEntry(9, 0xA001u, 0x55667788u);
|
||||
slots[9] = new ShortcutEntry(9, 0xB001u, 0x55667788u);
|
||||
|
||||
ShortcutMutation[] plan = ShortcutDropPlanner.PlanFullStackMerge(
|
||||
slots, 0xA001u, 0xB001u);
|
||||
|
|
@ -133,9 +133,8 @@ public sealed class ShortcutDropPlannerTests
|
|||
Assert.Equal(new[]
|
||||
{
|
||||
ShortcutMutation.Remove(2),
|
||||
ShortcutMutation.Add(new ShortcutEntry(2, 0xB001u, 0x11223344u)),
|
||||
ShortcutMutation.Remove(9),
|
||||
ShortcutMutation.Add(new ShortcutEntry(9, 0xB001u, 0x55667788u)),
|
||||
ShortcutMutation.Add(new ShortcutEntry(2, 0xB001u, 0x11223344u)),
|
||||
}, plan);
|
||||
}
|
||||
|
||||
|
|
|
|||
60
tests/AcDream.Core.Tests/Items/StackMergePlannerTests.cs
Normal file
60
tests/AcDream.Core.Tests/Items/StackMergePlannerTests.cs
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
using AcDream.Core.Items;
|
||||
|
||||
namespace AcDream.Core.Tests.Items;
|
||||
|
||||
public sealed class StackMergePlannerTests
|
||||
{
|
||||
[Fact]
|
||||
public void MatchingStacks_TransfersRequestedAmount()
|
||||
{
|
||||
StackMergePlan? plan = StackMergePlanner.Plan(
|
||||
Item(1, size: 40), Item(2, size: 20),
|
||||
readyForInventoryRequest: true, requestedAmount: 15);
|
||||
|
||||
Assert.Equal(new StackMergePlan(1, 2, 15), plan);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Transfer_IsLimitedByDestinationCapacity()
|
||||
{
|
||||
StackMergePlan? plan = StackMergePlanner.Plan(
|
||||
Item(1, size: 40), Item(2, size: 95),
|
||||
readyForInventoryRequest: true, requestedAmount: 40);
|
||||
|
||||
Assert.Equal(new StackMergePlan(1, 2, 5), plan);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ZeroRequestedAmount_UsesWholeSourceStack()
|
||||
{
|
||||
StackMergePlan? plan = StackMergePlanner.Plan(
|
||||
Item(1, size: 7), Item(2, size: 20),
|
||||
readyForInventoryRequest: true, requestedAmount: 0);
|
||||
|
||||
Assert.Equal(new StackMergePlan(1, 2, 7), plan);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, 10u, 10, 0, 100, 100)]
|
||||
[InlineData(true, 11u, 10, 0, 100, 100)]
|
||||
[InlineData(true, 10u, 10, 1, 100, 100)]
|
||||
[InlineData(true, 10u, 10, 0, 1, 100)]
|
||||
[InlineData(true, 10u, 10, 0, 100, 1)]
|
||||
[InlineData(true, 10u, 100, 0, 100, 100)]
|
||||
public void IllegalMerge_ReturnsNoPlan(
|
||||
bool ready,
|
||||
uint targetWcid,
|
||||
int targetSize,
|
||||
int sourceTradeState,
|
||||
int sourceMax,
|
||||
int targetMax)
|
||||
{
|
||||
var source = new StackMergeItem(1, 10, 10, sourceMax, sourceTradeState);
|
||||
var target = new StackMergeItem(2, targetWcid, targetSize, targetMax, 0);
|
||||
|
||||
Assert.Null(StackMergePlanner.Plan(source, target, ready, requestedAmount: 10));
|
||||
}
|
||||
|
||||
private static StackMergeItem Item(uint id, int size)
|
||||
=> new(id, WeenieClassId: 10, StackSize: size, MaxStackSize: 100, TradeState: 0);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue