feat(inventory): port retail item giving (#216)
Preserve SmartBox drag-release coordinates, route the picked 3-D target through the retail AttemptPlaceIn3D policy, and send authoritative GiveObject requests with the selected stack quantity. Honor PlayerDescription's player secure-trade option and document the remaining trade-system boundary. Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
parent
a944e49b9c
commit
30d294506c
13 changed files with 421 additions and 10 deletions
|
|
@ -21,10 +21,12 @@ public sealed class ItemInteractionControllerTests
|
|||
public readonly List<(uint Item, uint Container, int Placement)> Puts = new();
|
||||
public readonly List<uint> Drops = new();
|
||||
public readonly List<(uint Item, uint Amount)> SplitDrops = new();
|
||||
public readonly List<(uint Target, uint Item, uint Amount)> Gives = new();
|
||||
public readonly List<string> Toasts = new();
|
||||
public readonly StackSplitQuantityState SplitQuantity = new();
|
||||
public uint SelectedObject;
|
||||
public bool NonCombatMode;
|
||||
public bool DragOnPlayerOpensSecureTrade = true;
|
||||
public long Now = 1_000;
|
||||
|
||||
public Harness()
|
||||
|
|
@ -59,7 +61,9 @@ public sealed class ItemInteractionControllerTests
|
|||
stackSplitQuantity: SplitQuantity,
|
||||
inNonCombatMode: () => NonCombatMode,
|
||||
sendPutItemInContainer: (item, container, placement) =>
|
||||
Puts.Add((item, container, placement)));
|
||||
Puts.Add((item, container, placement)),
|
||||
sendGive: (target, item, amount) => Gives.Add((target, item, amount)),
|
||||
dragOnPlayerOpensSecureTrade: () => DragOnPlayerOpensSecureTrade);
|
||||
}
|
||||
|
||||
public ItemInteractionController Controller { get; }
|
||||
|
|
@ -697,6 +701,110 @@ public sealed class ItemInteractionControllerTests
|
|||
Assert.Equal(0u, h.Objects.Get(0x50000A07u)!.ContainerId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InventoryDragOnNpc_sendsGiveWithoutOptimisticInventoryMutation()
|
||||
{
|
||||
var h = new Harness();
|
||||
const uint item = 0x50000A71u;
|
||||
const uint npc = 0x50001001u;
|
||||
h.AddContained(item);
|
||||
h.Objects.AddOrUpdate(new ClientObject
|
||||
{
|
||||
ObjectId = npc,
|
||||
Name = "Starter Exit Warden",
|
||||
Type = ItemType.Creature,
|
||||
});
|
||||
var payload = new ItemDragPayload(
|
||||
item, ItemDragSource.Inventory, SourceSlot: 0, SourceCell: new UiItemSlot());
|
||||
|
||||
Assert.True(h.Controller.PlaceIn3D(payload, npc));
|
||||
|
||||
Assert.Equal(new[] { (npc, item, 1u) }, h.Gives);
|
||||
Assert.Empty(h.Drops);
|
||||
Assert.Equal(Pack, h.Objects.Get(item)!.ContainerId);
|
||||
Assert.Equal(1, h.Objects.Get(item)!.StackSize);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectedPartialStackDragOnNpc_sendsSelectedGiveAmount()
|
||||
{
|
||||
var h = new Harness();
|
||||
const uint item = 0x50000A72u;
|
||||
const uint npc = 0x50001002u;
|
||||
h.AddContained(item, value => value.StackSize = 10);
|
||||
h.Objects.AddOrUpdate(new ClientObject
|
||||
{
|
||||
ObjectId = npc,
|
||||
Type = ItemType.Creature,
|
||||
});
|
||||
h.SelectedObject = item;
|
||||
h.SplitQuantity.Reset(10u);
|
||||
h.SplitQuantity.SetValue(3u);
|
||||
var payload = new ItemDragPayload(
|
||||
item, ItemDragSource.Inventory, SourceSlot: 0, SourceCell: new UiItemSlot());
|
||||
|
||||
Assert.True(h.Controller.PlaceIn3D(payload, npc));
|
||||
|
||||
Assert.Equal(new[] { (npc, item, 3u) }, h.Gives);
|
||||
Assert.Equal(10, h.Objects.Get(item)!.StackSize);
|
||||
Assert.Equal(Pack, h.Objects.Get(item)!.ContainerId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InventoryDragOnNonCreature_usesRetailGroundFallback()
|
||||
{
|
||||
var h = new Harness();
|
||||
const uint item = 0x50000A73u;
|
||||
const uint scenery = 0x50002001u;
|
||||
h.AddContained(item);
|
||||
h.Objects.AddOrUpdate(new ClientObject
|
||||
{
|
||||
ObjectId = scenery,
|
||||
Type = ItemType.Misc,
|
||||
});
|
||||
var payload = new ItemDragPayload(
|
||||
item, ItemDragSource.Inventory, SourceSlot: 0, SourceCell: new UiItemSlot());
|
||||
|
||||
Assert.True(h.Controller.PlaceIn3D(payload, scenery));
|
||||
|
||||
Assert.Empty(h.Gives);
|
||||
Assert.Equal(new[] { item }, h.Drops);
|
||||
Assert.Equal(0u, h.Objects.Get(item)!.ContainerId);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true, false)]
|
||||
[InlineData(false, true)]
|
||||
public void InventoryDragOnPlayer_honorsRetailSecureTradeOption(
|
||||
bool opensSecureTrade,
|
||||
bool sendsGive)
|
||||
{
|
||||
var h = new Harness { DragOnPlayerOpensSecureTrade = opensSecureTrade };
|
||||
const uint item = 0x50000A74u;
|
||||
const uint targetPlayer = 0x50003001u;
|
||||
h.AddContained(item);
|
||||
h.Objects.AddOrUpdate(new ClientObject
|
||||
{
|
||||
ObjectId = targetPlayer,
|
||||
Type = ItemType.Creature,
|
||||
PublicWeenieBitfield = (uint)PublicWeenieFlags.Player,
|
||||
});
|
||||
var payload = new ItemDragPayload(
|
||||
item, ItemDragSource.Inventory, SourceSlot: 0, SourceCell: new UiItemSlot());
|
||||
|
||||
bool result = h.Controller.PlaceIn3D(payload, targetPlayer);
|
||||
|
||||
Assert.Equal(sendsGive, result);
|
||||
if (sendsGive)
|
||||
Assert.Equal(new[] { (targetPlayer, item, 1u) }, h.Gives);
|
||||
else
|
||||
{
|
||||
Assert.Empty(h.Gives);
|
||||
Assert.Contains(h.Toasts, text => text.Contains("Secure trade", StringComparison.Ordinal));
|
||||
}
|
||||
Assert.Equal(Pack, h.Objects.Get(item)!.ContainerId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectedPartialStackDragOutsideUi_splitsWithoutMovingOriginal()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue