feat(ui): finish retail toolbar controls
Discover all seven panel launchers from their DAT panel-id attributes, route mounted panels through event-driven retained window state, and ghost unavailable panels. Port Use and Examine selection/target behavior with exact Appraise dispatch and retail cursor modes. Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
parent
21fefce0e0
commit
d3d1c895a0
19 changed files with 478 additions and 72 deletions
|
|
@ -157,6 +157,33 @@ public sealed class CursorFeedbackControllerTests
|
|||
Assert.Equal(CursorFeedbackKind.TargetPending, c.Update(root).Kind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenericUseAndExamineModes_driveRetailGlobalCursors()
|
||||
{
|
||||
var objects = SeedTargetObjects();
|
||||
var interaction = new ItemInteractionController(
|
||||
objects,
|
||||
playerGuid: () => Player,
|
||||
sendUse: null,
|
||||
sendUseWithTarget: null,
|
||||
sendWield: null,
|
||||
sendDrop: null);
|
||||
var c = new CursorFeedbackController(interaction);
|
||||
|
||||
Assert.True(interaction.UseSelectedOrEnterMode(0));
|
||||
Assert.Equal(RetailGlobalCursorKind.Use,
|
||||
c.Resolve(new CursorFeedbackSnapshot()).GlobalKind);
|
||||
Assert.Equal(RetailGlobalCursorKind.UseFound,
|
||||
c.Resolve(new CursorFeedbackSnapshot(HoverTargetGuid: Target)).GlobalKind);
|
||||
|
||||
interaction.CancelTargetMode();
|
||||
Assert.True(interaction.ExamineSelectedOrEnterMode(0));
|
||||
Assert.Equal(RetailGlobalCursorKind.Examine,
|
||||
c.Resolve(new CursorFeedbackSnapshot()).GlobalKind);
|
||||
Assert.Equal(RetailGlobalCursorKind.ExamineFound,
|
||||
c.Resolve(new CursorFeedbackSnapshot(HoverTargetGuid: Target)).GlobalKind);
|
||||
}
|
||||
|
||||
public static TheoryData<CursorFeedbackSnapshot, RetailGlobalCursorKind> GlobalCursorCases => new()
|
||||
{
|
||||
{ new(), RetailGlobalCursorKind.Default },
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ public sealed class ItemInteractionControllerTests
|
|||
{
|
||||
public readonly ClientObjectTable Objects = new();
|
||||
public readonly List<uint> Uses = new();
|
||||
public readonly List<uint> Examines = new();
|
||||
public readonly List<(uint Source, uint Target)> UseWithTarget = new();
|
||||
public readonly List<(uint Item, uint Mask)> Wields = new();
|
||||
public readonly List<uint> Drops = new();
|
||||
|
|
@ -45,6 +46,7 @@ public sealed class ItemInteractionControllerTests
|
|||
Objects,
|
||||
playerGuid: () => Player,
|
||||
sendUse: Uses.Add,
|
||||
sendExamine: Examines.Add,
|
||||
sendUseWithTarget: (source, target) => UseWithTarget.Add((source, target)),
|
||||
sendWield: (item, mask) => Wields.Add((item, mask)),
|
||||
sendDrop: Drops.Add,
|
||||
|
|
@ -116,6 +118,38 @@ public sealed class ItemInteractionControllerTests
|
|||
Assert.Equal(new[] { (0x50000A01u, Player) }, h.UseWithTarget);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToolbarUse_withoutSelection_armsOneShotUseMode()
|
||||
{
|
||||
var h = new Harness();
|
||||
h.AddContained(0x50000A10u, item => item.Useability = ItemUseability.Contained);
|
||||
|
||||
Assert.True(h.Controller.UseSelectedOrEnterMode(0u));
|
||||
Assert.True(h.Controller.IsAnyTargetModeActive);
|
||||
Assert.Equal(InteractionModeKind.Use, h.Controller.InteractionState.Current.Kind);
|
||||
|
||||
Assert.Equal(ItemPrimaryClickResult.ConsumedSuccess,
|
||||
h.Controller.OfferPrimaryClick(0x50000A10u));
|
||||
Assert.Equal(new[] { 0x50000A10u }, h.Uses);
|
||||
Assert.False(h.Controller.IsAnyTargetModeActive);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToolbarExamine_usesSelectionOrArmsOneShotExamineMode()
|
||||
{
|
||||
var h = new Harness();
|
||||
|
||||
Assert.True(h.Controller.ExamineSelectedOrEnterMode(Player));
|
||||
Assert.Equal(new[] { Player }, h.Examines);
|
||||
|
||||
Assert.True(h.Controller.ExamineSelectedOrEnterMode(0u));
|
||||
Assert.Equal(InteractionModeKind.Examine, h.Controller.InteractionState.Current.Kind);
|
||||
Assert.Equal(ItemPrimaryClickResult.ConsumedSuccess,
|
||||
h.Controller.OfferPrimaryClick(Pack));
|
||||
Assert.Equal(new[] { Player, Pack }, h.Examines);
|
||||
Assert.False(h.Controller.IsAnyTargetModeActive);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelfTarget_sendsUseWithTargetAndClearsTargetMode()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -20,6 +20,18 @@ public class ToolbarControllerTests
|
|||
private static readonly uint[] CombatIds = { 0x10000192u, 0x10000193u, 0x10000194u, 0x10000195u };
|
||||
private const uint CharacterButtonId = 0x10000199u;
|
||||
private const uint InventoryButtonId = 0x100001B1u;
|
||||
private const uint UseButtonId = 0x1000019Du;
|
||||
private const uint ExamineButtonId = 0x100001A5u;
|
||||
private static readonly (uint ElementId, uint PanelId)[] PanelButtons =
|
||||
{
|
||||
(0x10000197u, 12u),
|
||||
(0x10000198u, 13u),
|
||||
(CharacterButtonId, RetailPanelCatalog.Character),
|
||||
(0x1000055Au, 25u),
|
||||
(0x1000019Au, 16u),
|
||||
(0x1000019Bu, 10u),
|
||||
(InventoryButtonId, RetailPanelCatalog.Inventory),
|
||||
};
|
||||
|
||||
private static (ImportedLayout layout, Dictionary<uint, UiItemList> slots,
|
||||
Dictionary<uint, UiElement> indicators) FakeToolbar()
|
||||
|
|
@ -37,8 +49,10 @@ public class ToolbarControllerTests
|
|||
AddButton(id);
|
||||
indicators[id] = dict[id];
|
||||
}
|
||||
AddButton(CharacterButtonId);
|
||||
AddButton(InventoryButtonId);
|
||||
foreach (var (elementId, panelId) in PanelButtons)
|
||||
AddButton(elementId, panelId);
|
||||
AddButton(UseButtonId);
|
||||
AddButton(ExamineButtonId);
|
||||
return (new ImportedLayout(root, dict), slots, indicators);
|
||||
|
||||
void AddSlot(uint id)
|
||||
|
|
@ -47,7 +61,7 @@ public class ToolbarControllerTests
|
|||
dict[id] = list; slots[id] = list; root.AddChild(list);
|
||||
}
|
||||
|
||||
void AddButton(uint id)
|
||||
void AddButton(uint id, uint? panelId = null)
|
||||
{
|
||||
var info = new ElementInfo
|
||||
{
|
||||
|
|
@ -59,6 +73,25 @@ public class ToolbarControllerTests
|
|||
};
|
||||
info.StateMedia["Normal"] = (0x1u, 1);
|
||||
info.StateMedia["Highlight"] = (0x2u, 1);
|
||||
info.StateMedia["Ghosted"] = (0x3u, 1);
|
||||
if (panelId is { } value)
|
||||
{
|
||||
info.States[UiStateInfo.DirectStateId] = new UiStateInfo
|
||||
{
|
||||
Id = UiStateInfo.DirectStateId,
|
||||
Properties = new UiPropertyBag
|
||||
{
|
||||
Values =
|
||||
{
|
||||
[0x10000029u] = new UiPropertyValue
|
||||
{
|
||||
Kind = UiPropertyKind.Enum,
|
||||
UnsignedValue = value,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
var button = new UiButton(info, _ => (0u, 0, 0)) { Width = 32, Height = 32 };
|
||||
dict[id] = button;
|
||||
root.AddChild(button);
|
||||
|
|
@ -140,25 +173,30 @@ public class ToolbarControllerTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void WindowToggleButtons_clickCallbacks_fire()
|
||||
public void PanelButtons_bindByDatPanelId_andGhostUnavailablePanels()
|
||||
{
|
||||
var (layout, _, _) = FakeToolbar();
|
||||
var repo = new ClientObjectTable();
|
||||
int inventoryClicks = 0;
|
||||
int characterClicks = 0;
|
||||
var toggles = new List<uint>();
|
||||
|
||||
var ctrl = ToolbarController.Bind(layout, repo,
|
||||
() => Array.Empty<ShortcutEntry>(),
|
||||
iconIds: (_,_,_,_,_) => 0u, useItem: _ => { });
|
||||
ctrl.BindWindowToggles(
|
||||
toggleInventory: () => inventoryClicks++,
|
||||
toggleCharacter: () => characterClicks++);
|
||||
ctrl.BindPanelButtons(
|
||||
panelId => panelId is RetailPanelCatalog.Inventory or RetailPanelCatalog.Character,
|
||||
toggles.Add);
|
||||
|
||||
((UiButton)layout.FindElement(InventoryButtonId)!).OnEvent(new UiEvent(0u, null, UiEventType.Click));
|
||||
((UiButton)layout.FindElement(CharacterButtonId)!).OnEvent(new UiEvent(0u, null, UiEventType.Click));
|
||||
|
||||
Assert.Equal(1, inventoryClicks);
|
||||
Assert.Equal(1, characterClicks);
|
||||
Assert.Equal(new[] { RetailPanelCatalog.Inventory, RetailPanelCatalog.Character }, toggles);
|
||||
foreach (var (elementId, panelId) in PanelButtons)
|
||||
{
|
||||
var button = (UiButton)layout.FindElement(elementId)!;
|
||||
Assert.Equal(
|
||||
panelId is RetailPanelCatalog.Inventory or RetailPanelCatalog.Character,
|
||||
button.Enabled);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -195,9 +233,9 @@ public class ToolbarControllerTests
|
|||
iconIds: (_,_,_,_,_) => 0u,
|
||||
useItem: _ => { },
|
||||
itemInteraction: interaction);
|
||||
ctrl.BindWindowToggles(
|
||||
toggleInventory: () => inventoryClicks++,
|
||||
toggleCharacter: () => { });
|
||||
ctrl.BindPanelButtons(
|
||||
panelId => panelId == RetailPanelCatalog.Inventory,
|
||||
_ => inventoryClicks++);
|
||||
interaction.ActivateItem(kit);
|
||||
|
||||
((UiButton)layout.FindElement(InventoryButtonId)!).OnEvent(new UiEvent(0u, null, UiEventType.Click));
|
||||
|
|
@ -271,19 +309,82 @@ public class ToolbarControllerTests
|
|||
var inventoryButton = (UiButton)layout.FindElement(InventoryButtonId)!;
|
||||
var characterButton = (UiButton)layout.FindElement(CharacterButtonId)!;
|
||||
|
||||
ctrl.SetInventoryOpen(true);
|
||||
ctrl.SetCharacterOpen(true);
|
||||
ctrl.BindPanelButtons(_ => true, _ => { });
|
||||
ctrl.SetPanelOpen(RetailPanelCatalog.Inventory, true);
|
||||
ctrl.SetPanelOpen(RetailPanelCatalog.Character, true);
|
||||
|
||||
Assert.Equal("Highlight", inventoryButton.ActiveState);
|
||||
Assert.Equal("Highlight", characterButton.ActiveState);
|
||||
|
||||
ctrl.SetInventoryOpen(false);
|
||||
ctrl.SetCharacterOpen(false);
|
||||
ctrl.SetPanelOpen(RetailPanelCatalog.Inventory, false);
|
||||
ctrl.SetPanelOpen(RetailPanelCatalog.Character, false);
|
||||
|
||||
Assert.Equal("Normal", inventoryButton.ActiveState);
|
||||
Assert.Equal("Normal", characterButton.ActiveState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RetailFixture_panelButtonsExposeExactDatPanelIds()
|
||||
{
|
||||
ImportedLayout layout = FixtureLoader.LoadToolbar();
|
||||
|
||||
foreach (var (elementId, panelId) in PanelButtons)
|
||||
{
|
||||
var button = Assert.IsType<UiButton>(layout.FindElement(elementId));
|
||||
Assert.True(button.TryGetEnumAttribute(0x10000029u, out uint actual));
|
||||
Assert.Equal(panelId, actual);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UseAndExamineButtons_dispatchSelectedObjectPolicy()
|
||||
{
|
||||
const uint player = 0x50000001u;
|
||||
const uint pack = 0x50000002u;
|
||||
const uint item = 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.MoveItem(pack, player, 0);
|
||||
repo.AddOrUpdate(new ClientObject
|
||||
{
|
||||
ObjectId = item,
|
||||
Type = ItemType.Misc,
|
||||
Useability = ItemUseability.Contained,
|
||||
});
|
||||
repo.MoveItem(item, pack, 0);
|
||||
var uses = new List<uint>();
|
||||
var examines = new List<uint>();
|
||||
uint selected = item;
|
||||
var interaction = new ItemInteractionController(
|
||||
repo,
|
||||
() => player,
|
||||
sendUse: uses.Add,
|
||||
sendUseWithTarget: null,
|
||||
sendWield: null,
|
||||
sendDrop: null,
|
||||
sendExamine: examines.Add);
|
||||
ToolbarController.Bind(
|
||||
layout,
|
||||
repo,
|
||||
() => Array.Empty<ShortcutEntry>(),
|
||||
iconIds: (_, _, _, _, _) => 0u,
|
||||
useItem: _ => { },
|
||||
itemInteraction: interaction,
|
||||
selectedObjectId: () => selected);
|
||||
|
||||
((UiButton)layout.FindElement(UseButtonId)!).OnClick!.Invoke();
|
||||
((UiButton)layout.FindElement(ExamineButtonId)!).OnClick!.Invoke();
|
||||
|
||||
Assert.Equal(new[] { item }, uses);
|
||||
Assert.Equal(new[] { item }, examines);
|
||||
|
||||
selected = 0;
|
||||
((UiButton)layout.FindElement(ExamineButtonId)!).OnClick!.Invoke();
|
||||
Assert.Equal(InteractionModeKind.Examine, interaction.InteractionState.Current.Kind);
|
||||
}
|
||||
|
||||
// ── C1: combat-mode indicator tests ─────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -201,6 +201,31 @@ public sealed class RetailWindowManagerTests
|
|||
Assert.Equal(1, controller.DisposeCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VisibilityEvent_tracksEveryRegisteredWindowTransition()
|
||||
{
|
||||
var root = new UiRoot { Width = 800, Height = 600 };
|
||||
var inventory = new UiPanel { Width = 100, Height = 100 };
|
||||
root.AddChild(inventory);
|
||||
root.RegisterWindow("inventory", inventory);
|
||||
var transitions = new List<(string Name, bool Visible)>();
|
||||
root.WindowManager.WindowVisibilityChanged +=
|
||||
(name, visible) => transitions.Add((name, visible));
|
||||
|
||||
Assert.True(root.HideWindow("inventory"));
|
||||
Assert.True(root.ShowWindow("inventory"));
|
||||
Assert.False(root.ToggleWindow("inventory"));
|
||||
|
||||
Assert.Equal(
|
||||
new[]
|
||||
{
|
||||
("inventory", false),
|
||||
("inventory", true),
|
||||
("inventory", false),
|
||||
},
|
||||
transitions);
|
||||
}
|
||||
|
||||
private sealed class RecordingController : IRetainedPanelController
|
||||
{
|
||||
public int ShownCount { get; private set; }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue