feat(ui): port retail toolbar ammo count

Resolve stacked thrown weapons versus separately equipped missile ammo exactly like gmToolbarUI, normalize a zero stack to one, and refresh the authored missile indicator from equipment and stack events. Keep the decision in pure Core with retained and real-DAT conformance coverage.

Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-11 12:29:51 +02:00
parent 400f01eef1
commit 564d39dfea
9 changed files with 330 additions and 7 deletions

View file

@ -336,6 +336,17 @@ public class ToolbarControllerTests
}
}
[Fact]
public void RetailFixture_ammoNumberUsesAuthoredMissileIndicatorElement()
{
ImportedLayout layout = FixtureLoader.LoadToolbar();
var ammoIndicator = Assert.IsType<UiButton>(layout.FindElement(0x10000194u));
Assert.Equal(55f, ammoIndicator.Width);
Assert.Equal(58f, ammoIndicator.Height);
}
[Fact]
public void UseAndExamineButtons_dispatchSelectedObjectPolicy()
{
@ -387,6 +398,76 @@ public class ToolbarControllerTests
// ── C1: combat-mode indicator tests ─────────────────────────────────────
[Fact]
public void AmmoNumber_usesStackableMissileWeapon_andTracksStackChanges()
{
const uint player = 0x50000001u;
const uint thrownWeapon = 0x50000002u;
const uint separateAmmo = 0x50000003u;
var (layout, _, _) = FakeToolbar();
var repo = new ClientObjectTable();
repo.AddOrUpdate(new ClientObject { ObjectId = player, Type = ItemType.Creature });
repo.AddOrUpdate(new ClientObject
{
ObjectId = thrownWeapon,
StackSize = 7,
StackSizeMax = 25,
});
repo.MoveItem(thrownWeapon, player, newEquipLocation: EquipMask.MissileWeapon);
repo.AddOrUpdate(new ClientObject
{
ObjectId = separateAmmo,
StackSize = 80,
StackSizeMax = 100,
});
repo.MoveItem(separateAmmo, player, newEquipLocation: EquipMask.MissileAmmo);
ToolbarController.Bind(
layout,
repo,
() => Array.Empty<ShortcutEntry>(),
iconIds: (_, _, _, _, _) => 0u,
useItem: _ => { },
playerGuid: () => player);
var ammoButton = (UiButton)layout.FindElement(0x10000194u)!;
Assert.Equal("7", ammoButton.Label);
Assert.True(repo.UpdateStackSize(thrownWeapon, 6, value: 0));
Assert.Equal("6", ammoButton.Label);
}
[Fact]
public void AmmoNumber_forLauncher_usesAmmoSlot_andClearsOnUnwield()
{
const uint player = 0x50000001u;
const uint pack = 0x50000010u;
const uint bow = 0x50000002u;
const uint arrows = 0x50000003u;
var (layout, _, _) = FakeToolbar();
var repo = new ClientObjectTable();
repo.AddOrUpdate(new ClientObject { ObjectId = player, Type = ItemType.Creature });
repo.AddOrUpdate(new ClientObject { ObjectId = pack, Type = ItemType.Container });
repo.AddOrUpdate(new ClientObject { ObjectId = bow, StackSize = 1, StackSizeMax = 1 });
repo.MoveItem(bow, player, newEquipLocation: EquipMask.MissileWeapon);
repo.AddOrUpdate(new ClientObject { ObjectId = arrows, StackSize = 42, StackSizeMax = 100 });
repo.MoveItem(arrows, player, newEquipLocation: EquipMask.MissileAmmo);
ToolbarController.Bind(
layout,
repo,
() => Array.Empty<ShortcutEntry>(),
iconIds: (_, _, _, _, _) => 0u,
useItem: _ => { },
playerGuid: () => player);
var ammoButton = (UiButton)layout.FindElement(0x10000194u)!;
Assert.Equal("42", ammoButton.Label);
Assert.True(repo.MoveItem(arrows, pack));
Assert.Null(ammoButton.Label);
}
[Fact]
public void CombatIndicators_allDispatchSharedRetailToggleCommand()
{