feat(inventory): port retail weapon switching

Sequence primary weapon replacement through the server-confirmed AutoWield transaction: return the occupied weapon to the player pack, wait for its move event, then wield the requested item. Route authoritative CombatMode property updates so peace stays peace and war adopts the new weapon stance without client-synthesized animation.

Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-12 23:06:31 +02:00
parent 8e9c538519
commit 17b5712d53
14 changed files with 736 additions and 155 deletions

View file

@ -11,6 +11,8 @@ public sealed class RetailUiInteractionFlowTests
private const uint Player = 0x50000001u;
private const uint Hauberk = 0x50001001u;
private const uint HealthKit = 0x50001002u;
private const uint Sword = 0x50001003u;
private const uint Bow = 0x50001004u;
private const uint SlotsButtonId = 0x100005BEu;
private const uint ChestArmorSlotId = 0x100005ACu;
// USEABLE_SOURCE_CONTAINED_TARGET_REMOTE_OR_SELF — the classic healing-kit value.
@ -32,6 +34,7 @@ public sealed class RetailUiInteractionFlowTests
public readonly List<uint> Uses = new();
public readonly List<(uint Source, uint Target)> UseWithTarget = new();
public readonly List<(uint Item, uint Mask)> Wields = new();
public readonly List<(uint Item, uint Container, int Placement)> Puts = new();
public readonly AcDream.Core.Selection.SelectionState Selection = new();
public readonly List<uint> Drops = new();
public long Now = 10_000;
@ -165,7 +168,9 @@ public sealed class RetailUiInteractionFlowTests
sendUseWithTarget: (source, target) => UseWithTarget.Add((source, target)),
sendWield: (item, mask) => Wields.Add((item, mask)),
sendDrop: Drops.Add,
nowMs: () => Now);
nowMs: () => Now,
sendPutItemInContainer: (item, container, placement) =>
Puts.Add((item, container, placement)));
InventoryController.Bind(
Layout,
@ -285,6 +290,46 @@ public sealed class RetailUiInteractionFlowTests
Assert.True(state.Success, state.Message);
}
[Fact]
public void InventoryDoubleClick_bow_replacesSwordOnlyAfterConfirmedUnwield()
{
var h = new Harness();
h.Objects.AddOrUpdate(new ClientObject
{
ObjectId = Sword,
Name = "Sword",
Type = ItemType.MeleeWeapon,
CombatUse = 1,
ValidLocations = EquipMask.MeleeWeapon,
IconId = 0x06001234u,
});
h.Objects.MoveItem(Sword, Player, -1, EquipMask.MeleeWeapon);
h.Objects.AddOrUpdate(new ClientObject
{
ObjectId = Bow,
Name = "Bow",
Type = ItemType.MissileWeapon,
CombatUse = 1,
ValidLocations = EquipMask.MissileWeapon,
IconId = 0x06001235u,
});
h.Objects.MoveItem(Bow, Player, 0);
h.BindInventoryInteraction();
var probe = h.Probe();
Assert.True(probe.DoubleClickItem(Bow, ItemDragSource.Inventory));
Assert.Equal(new[] { (Sword, Player, 0) }, h.Puts);
Assert.Empty(h.Wields);
Assert.Equal(EquipMask.MeleeWeapon,
h.Objects.Get(Sword)!.CurrentlyEquippedLocation);
h.Objects.MoveItem(Sword, Player, 0, EquipMask.None);
Assert.Equal(new[] { (Bow, (uint)EquipMask.MissileWeapon) }, h.Wields);
Assert.Equal(EquipMask.MissileWeapon,
h.Objects.Get(Bow)!.CurrentlyEquippedLocation);
}
[Fact]
public void InventoryTargetUse_thenMainPackClick_selfTargetsThroughUiRoot()
{