fix(inventory): sequence secondary wield blockers

Preserve retail AmmoType from CreateObject and port BlocksUseOfShield so bow, caster, and two-handed switches remove incompatible shields, while mismatched missile ammo is also returned to the pack. Each blocker remains server-confirmed before AutoWield re-enters.

Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-12 23:12:07 +02:00
parent 17b5712d53
commit 815ce0dec4
14 changed files with 206 additions and 16 deletions

View file

@ -523,6 +523,115 @@ public sealed class ItemInteractionControllerTests
h.Objects.Get(sword)!.CurrentlyEquippedLocation);
}
[Fact]
public void WeaponReplacement_bow_removesPrimaryThenIncompatibleShield()
{
var h = new Harness();
const uint sword = 0x50000B31u;
const uint shield = 0x50000B32u;
const uint bow = 0x50000B33u;
h.Objects.AddOrUpdate(new ClientObject
{
ObjectId = sword,
Type = ItemType.MeleeWeapon,
CombatUse = 1,
ValidLocations = EquipMask.MeleeWeapon,
});
h.Objects.MoveItem(sword, Player, -1, EquipMask.MeleeWeapon);
h.Objects.AddOrUpdate(new ClientObject
{
ObjectId = shield,
Type = ItemType.Armor,
CombatUse = 4,
ValidLocations = EquipMask.Shield,
});
h.Objects.MoveItem(shield, Player, -1, EquipMask.Shield);
h.AddContained(bow, item =>
{
item.Type = ItemType.MissileWeapon;
item.CombatUse = 2;
item.AmmoType = 1;
item.ValidLocations = EquipMask.MissileWeapon;
});
Assert.True(h.Controller.ActivateItem(bow));
Assert.Equal(new[] { (sword, Player, 0) }, h.Puts);
h.Objects.MoveItem(sword, Player, 0, EquipMask.None);
Assert.Equal(
new[] { (sword, Player, 0), (shield, Player, 0) },
h.Puts);
Assert.Empty(h.Wields);
h.Objects.MoveItem(shield, Player, 0, EquipMask.None);
Assert.Equal(new[] { (bow, (uint)EquipMask.MissileWeapon) }, h.Wields);
}
[Fact]
public void WeaponReplacement_bow_removesMismatchedAmmoAfterPrimaryBlocker()
{
var h = new Harness();
const uint sword = 0x50000B41u;
const uint arrows = 0x50000B42u;
const uint bow = 0x50000B43u;
h.Objects.AddOrUpdate(new ClientObject
{
ObjectId = sword,
Type = ItemType.MeleeWeapon,
CombatUse = 1,
ValidLocations = EquipMask.MeleeWeapon,
});
h.Objects.MoveItem(sword, Player, -1, EquipMask.MeleeWeapon);
h.Objects.AddOrUpdate(new ClientObject
{
ObjectId = arrows,
Type = ItemType.MissileWeapon,
CombatUse = 3,
AmmoType = 2,
ValidLocations = EquipMask.MissileAmmo,
});
h.Objects.MoveItem(arrows, Player, -1, EquipMask.MissileAmmo);
h.AddContained(bow, item =>
{
item.Type = ItemType.MissileWeapon;
item.CombatUse = 2;
item.AmmoType = 1;
item.ValidLocations = EquipMask.MissileWeapon;
});
Assert.True(h.Controller.ActivateItem(bow));
h.Objects.MoveItem(sword, Player, 0, EquipMask.None);
Assert.Equal(
new[] { (sword, Player, 0), (arrows, Player, 0) },
h.Puts);
h.Objects.MoveItem(arrows, Player, 0, EquipMask.None);
Assert.Equal(new[] { (bow, (uint)EquipMask.MissileWeapon) }, h.Wields);
}
[Fact]
public void BlocksUseOfShield_matchesRetailCombatUseAndAmmoRules()
{
Assert.True(AutoWieldController.BlocksUseOfShield(new ClientObject
{
CombatUse = 2,
AmmoType = 1,
}));
Assert.False(AutoWieldController.BlocksUseOfShield(new ClientObject
{
CombatUse = 2,
AmmoType = 0,
}));
Assert.True(AutoWieldController.BlocksUseOfShield(new ClientObject
{
CombatUse = 5,
}));
Assert.True(AutoWieldController.BlocksUseOfShield(new ClientObject
{
Type = ItemType.Caster,
}));
}
[Fact]
public void InventoryDragOutsideUi_sendsDropAndMovesToWorldOptimistically()
{