fix(ui): share retail tab state binding
Replace the character window's synthesized tab sprites, labels, colors, and hit panels with the same DAT-authored Open/Closed state path used by Spellbook/Components. Preserve controller-owned page selection while letting UIElement state propagation own tab chrome and typography. Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
parent
1249ad03de
commit
15bb61e05d
5 changed files with 179 additions and 285 deletions
|
|
@ -775,73 +775,56 @@ public class CharacterStatControllerTests
|
|||
Assert.False(btn1.Visible, "raise button hidden after deselect");
|
||||
}
|
||||
|
||||
// ── Pass 2: Tab button states ─────────────────────────────────────────────
|
||||
// Tab sprites are added to layout.Root (NOT to the tab group elements), so
|
||||
// the tab groups keep Children.Count==0 and survive the page-visibility pass.
|
||||
// AddTabSpritesToRoot() adds 3 sprite UiTexts + 1 label UiText per tab to the
|
||||
// root when a spriteResolve is provided; nothing is added when spriteResolve=null.
|
||||
// ── Pass 2: DAT-authored tab states ───────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void TabButtons_NoSpriteResolve_AddsNoSpriteChildrenToRoot()
|
||||
public void CharacterTabs_UseImportedChromeWithoutSyntheticRootChildren()
|
||||
{
|
||||
// When spriteResolve is null, AddTabSpritesToRoot is not called — no sprites added.
|
||||
var layout = Fake(); // empty fake layout with just the root UiPanel
|
||||
int rootChildCountBefore = layout.Root.Children.Count;
|
||||
var layout = FixtureLoader.LoadCharacter();
|
||||
int rootChildCount = layout.Root.Children.Count;
|
||||
var attributes = Assert.IsType<UiText>(layout.FindElement(CharacterStatController.TabAttribId));
|
||||
var skills = Assert.IsType<UiText>(layout.FindElement(CharacterStatController.TabSkillsId));
|
||||
var titles = Assert.IsType<UiText>(layout.FindElement(CharacterStatController.TabTitlesId));
|
||||
|
||||
CharacterStatController.Bind(layout, SampleData.SampleCharacter, spriteResolve: null);
|
||||
|
||||
// No extra children added to root when spriteResolve is null.
|
||||
Assert.Equal(rootChildCountBefore, layout.Root.Children.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TabButtons_AttributesGroup_AddsOpenSpriteIdsToRoot()
|
||||
{
|
||||
// Attributes tab (isOpen=true): 3 sprite children with Open sprite ids on ROOT.
|
||||
// The tab group element itself has 0 children (sprites go to root, not the group).
|
||||
var layout = Fake(); // minimal fake
|
||||
Assert.Equal(3, attributes.Children.Count);
|
||||
Assert.Equal(3, skills.Children.Count);
|
||||
Assert.Equal(3, titles.Children.Count);
|
||||
|
||||
CharacterStatController.Bind(layout, SampleData.SampleCharacter,
|
||||
spriteResolve: id => (id, 16, 16));
|
||||
|
||||
// Root should contain the Open sprite ids among all tab sprite children.
|
||||
var sprites = layout.Root.Children.OfType<UiText>()
|
||||
.Where(t => t.BackgroundSprite != 0u).ToList();
|
||||
Assert.Contains(sprites, t => t.BackgroundSprite == 0x06005D92u); // Open left-cap
|
||||
Assert.Contains(sprites, t => t.BackgroundSprite == 0x06005D94u); // Open center
|
||||
Assert.Contains(sprites, t => t.BackgroundSprite == 0x06005D96u); // Open right-cap
|
||||
Assert.Equal(rootChildCount, layout.Root.Children.Count);
|
||||
Assert.Equal(RetailUiStateIds.Open, attributes.ActiveRetailStateId);
|
||||
Assert.Equal(RetailUiStateIds.Closed, skills.ActiveRetailStateId);
|
||||
Assert.Equal(RetailUiStateIds.Closed, titles.ActiveRetailStateId);
|
||||
Assert.Equal(
|
||||
new uint[] { 0x06005D92u, 0x06005D94u, 0x06005D96u },
|
||||
attributes.Children.Cast<UiDatElement>().Select(child => child.ActiveMedia().File));
|
||||
Assert.Equal(
|
||||
new uint[] { 0x06005D93u, 0x06005D95u, 0x06005D97u },
|
||||
skills.Children.Cast<UiDatElement>().Select(child => child.ActiveMedia().File));
|
||||
Assert.True(titles.ClickThrough);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TabButtons_SkillsAndTitles_AddClosedSpriteIdsToRoot()
|
||||
public void CharacterTabs_ClickUsesRetailStateColorAndPropagatesToChrome()
|
||||
{
|
||||
// Skills + Titles tabs (isOpen=false): Closed sprite ids appear on root.
|
||||
var layout = Fake();
|
||||
var layout = FixtureLoader.LoadCharacter();
|
||||
var attributes = Assert.IsType<UiText>(layout.FindElement(CharacterStatController.TabAttribId));
|
||||
var skills = Assert.IsType<UiText>(layout.FindElement(CharacterStatController.TabSkillsId));
|
||||
|
||||
CharacterStatController.Bind(layout, SampleData.SampleCharacter,
|
||||
spriteResolve: id => (id, 16, 16));
|
||||
skills.OnClick!();
|
||||
|
||||
var sprites = layout.Root.Children.OfType<UiText>()
|
||||
.Where(t => t.BackgroundSprite != 0u).ToList();
|
||||
Assert.Contains(sprites, t => t.BackgroundSprite == 0x06005D93u); // Closed left-cap
|
||||
Assert.Contains(sprites, t => t.BackgroundSprite == 0x06005D95u); // Closed center
|
||||
Assert.Contains(sprites, t => t.BackgroundSprite == 0x06005D97u); // Closed right-cap
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TabButtons_WithSpriteResolve_AddsAllThreeTabsToRoot()
|
||||
{
|
||||
// With spriteResolve: all 3 tabs inject 4 children each (3 sprites + 1 label)
|
||||
// = 12 sprite children total across 3 tabs on the root.
|
||||
var layout = Fake();
|
||||
|
||||
CharacterStatController.Bind(layout, SampleData.SampleCharacter,
|
||||
spriteResolve: id => (id, 16, 16));
|
||||
|
||||
// 3 tabs × 3 sprites = 9 sprite UiTexts on root.
|
||||
var sprites = layout.Root.Children.OfType<UiText>()
|
||||
.Where(t => t.BackgroundSprite != 0u).ToList();
|
||||
Assert.Equal(9, sprites.Count);
|
||||
Assert.Equal(RetailUiStateIds.Closed, attributes.ActiveRetailStateId);
|
||||
Assert.Equal(RetailUiStateIds.Open, skills.ActiveRetailStateId);
|
||||
Assert.Equal(127f / 255f, attributes.DefaultColor.X, 5);
|
||||
Assert.Equal(204f / 255f, skills.DefaultColor.X, 5);
|
||||
Assert.All(attributes.Children, child =>
|
||||
Assert.Equal(RetailUiStateIds.Closed, Assert.IsAssignableFrom<IUiDatStateful>(child).ActiveRetailStateId));
|
||||
Assert.All(skills.Children, child =>
|
||||
Assert.Equal(RetailUiStateIds.Open, Assert.IsAssignableFrom<IUiDatStateful>(child).ActiveRetailStateId));
|
||||
}
|
||||
|
||||
// ── Affordability helpers (GetRaiseCost) ──────────────────────────────────
|
||||
|
|
@ -934,12 +917,15 @@ public class CharacterStatControllerTests
|
|||
hiddenPage.AddChild(hiddenDuplicateList);
|
||||
root.AddChild(attrPage);
|
||||
root.AddChild(hiddenPage);
|
||||
var skillsTab = MakeTab(CharacterStatController.TabSkillsId, left: 92f);
|
||||
root.AddChild(skillsTab);
|
||||
|
||||
var layout = new ImportedLayout(root, new Dictionary<uint, UiElement>
|
||||
{
|
||||
[CharacterStatController.NameId] = name,
|
||||
// Mirrors the real import: the id dictionary can point at a hidden duplicate.
|
||||
[CharacterStatController.ListBoxId] = hiddenDuplicateList,
|
||||
[CharacterStatController.TabSkillsId] = skillsTab,
|
||||
});
|
||||
|
||||
CharacterStatController.Bind(layout, SampleData.SampleCharacter,
|
||||
|
|
@ -950,7 +936,7 @@ public class CharacterStatControllerTests
|
|||
|
||||
var ui = new UiRoot { Width = 300, Height = 600 };
|
||||
ui.AddChild(root);
|
||||
Assert.IsType<UiClickablePanel>(ui.Pick(132, 12));
|
||||
Assert.Same(skillsTab, ui.Pick(132, 12));
|
||||
|
||||
ui.OnMouseDown(UiMouseButton.Left, 132, 12);
|
||||
ui.OnMouseUp(UiMouseButton.Left, 132, 12);
|
||||
|
|
@ -1215,12 +1201,15 @@ public class CharacterStatControllerTests
|
|||
page.AddChild(list);
|
||||
page.AddChild(scrollbarShell);
|
||||
root.AddChild(page);
|
||||
var skillsTab = MakeTab(CharacterStatController.TabSkillsId, left: 92f);
|
||||
root.AddChild(skillsTab);
|
||||
|
||||
var layout = new ImportedLayout(root, new Dictionary<uint, UiElement>
|
||||
{
|
||||
[CharacterStatController.NameId] = name,
|
||||
[CharacterStatController.ListBoxId] = list,
|
||||
[CharacterStatController.ListScrollbarId] = scrollbarShell,
|
||||
[CharacterStatController.TabSkillsId] = skillsTab,
|
||||
});
|
||||
|
||||
CharacterStatController.Bind(layout, SampleData.SampleCharacter,
|
||||
|
|
@ -1589,10 +1578,13 @@ public class CharacterStatControllerTests
|
|||
|
||||
private static void ClickTab(ImportedLayout layout, float left)
|
||||
{
|
||||
var tab = layout.Root.Children.OfType<UiClickablePanel>()
|
||||
.Single(p => System.Math.Abs(p.Left - left) < 0.5f
|
||||
&& System.Math.Abs(p.Top) < 0.5f
|
||||
&& System.Math.Abs(p.Height - 25f) < 0.5f);
|
||||
uint id = left switch
|
||||
{
|
||||
0f => CharacterStatController.TabAttribId,
|
||||
92f => CharacterStatController.TabSkillsId,
|
||||
_ => CharacterStatController.TabTitlesId,
|
||||
};
|
||||
var tab = Assert.IsType<UiText>(layout.FindElement(id));
|
||||
tab.OnClick!();
|
||||
}
|
||||
|
||||
|
|
@ -1660,6 +1652,33 @@ public class CharacterStatControllerTests
|
|||
return new UiButton(info, static _ => (0u, 0, 0));
|
||||
}
|
||||
|
||||
private static UiText MakeTab(uint id, float left)
|
||||
{
|
||||
var info = new ElementInfo
|
||||
{
|
||||
Id = id,
|
||||
Type = 12,
|
||||
X = left,
|
||||
Width = 92f,
|
||||
Height = 25f,
|
||||
DefaultStateId = RetailUiStateIds.Closed,
|
||||
DefaultStateName = "Closed",
|
||||
};
|
||||
info.States[RetailUiStateIds.Closed] = new UiStateInfo
|
||||
{
|
||||
Id = RetailUiStateIds.Closed,
|
||||
Name = "Closed",
|
||||
PassToChildren = true,
|
||||
};
|
||||
info.States[RetailUiStateIds.Open] = new UiStateInfo
|
||||
{
|
||||
Id = RetailUiStateIds.Open,
|
||||
Name = "Open",
|
||||
PassToChildren = true,
|
||||
};
|
||||
return Assert.IsType<UiText>(DatWidgetFactory.Create(info, static _ => (0u, 0, 0), null));
|
||||
}
|
||||
|
||||
private static ImportedLayout Fake(params (uint id, UiElement e)[] items)
|
||||
{
|
||||
var dict = new Dictionary<uint, UiElement>();
|
||||
|
|
@ -1669,6 +1688,18 @@ public class CharacterStatControllerTests
|
|||
root.AddChild(e);
|
||||
dict[id] = e;
|
||||
}
|
||||
foreach ((uint id, float left) in new[]
|
||||
{
|
||||
(CharacterStatController.TabAttribId, 0f),
|
||||
(CharacterStatController.TabSkillsId, 92f),
|
||||
(CharacterStatController.TabTitlesId, 184f),
|
||||
})
|
||||
{
|
||||
if (dict.ContainsKey(id)) continue;
|
||||
UiText tab = MakeTab(id, left);
|
||||
root.AddChild(tab);
|
||||
dict[id] = tab;
|
||||
}
|
||||
return new ImportedLayout(root, dict);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue