feat(D.2b): Character window — value captions + tab bar sprites + footer legibility
Gap 1 — Header captions:
- 0x1000023A ("Character Level") above the level value: bound via LabelLeft().
- 0x10000234 ("Total Experience (XP):") left of total XP: bound via LabelLeft().
- 0x10000237/0x10000238 (XP-to-level label + value) are children of the UiMeter
(0x10000236, ConsumesDatChildren=true) and cannot be bound — documented in
code comments; XP meter fill still bound via Fill=XpFraction.
Gap 2 — Tab bar sprites:
- Tab group elements 0x10000228/229/538 are Type-12 UIElement_Text in the dat
(ConsumesDatChildren=true), so the three button children (left-cap, center,
right-cap) are consumed at import and absent from the widget tree. Old
SetTabState/SetButtonStateRecursive found no UiButton children to set.
- Fix: AddTabSprites() injects three UiText sprite-children per group using
the known RenderSurface ids confirmed from the retail UI layout dump:
Open (active) 0x06005D92/0x06005D94/0x06005D96
Closed (inactive) 0x06005D93/0x06005D95/0x06005D97
Source: dump nodes 0x10000439/0x100000E9/0x10000215 in layout 0x2100002E,
state_id 11=Closed, 12=Open per gmTabUI::SetActive(bool).
Gap 3 — Footer legibility:
- The shared footer child ids (0x1000024E etc.) appear in THREE footer-state
groups (A/B/C). ImportedLayout._byId stores the LAST duplicate = narrower
State B/C copies (145px labels). Fix: hide State B/C groups (footerB/footerC
Visible=false), walk State A container (0x10000240) positionally to bind the
wider State A labels (195px). FooterLine1Label now reads "Skill Credits
Available:" and FooterLine2Label reads "Unassigned Experience:" at full width.
Tests: 3 old tab-state tests (SetButtonStateRecursive expectation) replaced by
4 new sprite-injection tests + 2 caption-binding tests. Full suite: 676 pass,
0 fail (was 673 pass after 3 failures).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
defbde1f86
commit
99291595bb
3 changed files with 297 additions and 85 deletions
|
|
@ -514,51 +514,71 @@ public class CharacterStatControllerTests
|
|||
}
|
||||
|
||||
// ── Pass 2: Tab button states ─────────────────────────────────────────────
|
||||
// Tab groups are UiText (Type 12) with ConsumesDatChildren=true; their button
|
||||
// children are consumed at import time. AddTabSprites() injects 3 UiText
|
||||
// sprite-children per group when a spriteResolve is provided.
|
||||
|
||||
[Fact]
|
||||
public void TabButtons_AttributesGroupButtons_SetToOpenState()
|
||||
public void TabButtons_NoSpriteResolve_AddsNoSpriteChildren()
|
||||
{
|
||||
var tabGroup = new UiPanel(); // fake tab group container
|
||||
var btn1 = MakeButton();
|
||||
var btn2 = MakeButton();
|
||||
tabGroup.AddChild(btn1);
|
||||
tabGroup.AddChild(btn2);
|
||||
// When spriteResolve is null, AddTabSprites returns early — no children added.
|
||||
var tabGroup = new UiText();
|
||||
var layout = Fake((CharacterStatController.TabAttribId, tabGroup));
|
||||
|
||||
var layout = Fake((CharacterStatController.TabAttribId, tabGroup));
|
||||
CharacterStatController.Bind(layout, SampleData.SampleCharacter, spriteResolve: null);
|
||||
|
||||
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
|
||||
|
||||
// All UiButton children of the Attributes group should be "Open".
|
||||
Assert.Equal("Open", btn1.ActiveState);
|
||||
Assert.Equal("Open", btn2.ActiveState);
|
||||
Assert.Empty(tabGroup.Children);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TabButtons_SkillsGroupButtons_SetToClosedState()
|
||||
public void TabButtons_AttributesGroup_AddsThreeOpenSpriteChildren()
|
||||
{
|
||||
var tabGroup = new UiPanel();
|
||||
var btn = MakeButton();
|
||||
tabGroup.AddChild(btn);
|
||||
// Attributes tab (isOpen=true): three UiText sprite children with Open sprite ids.
|
||||
var tabGroup = new UiText();
|
||||
var layout = Fake((CharacterStatController.TabAttribId, tabGroup));
|
||||
|
||||
var layout = Fake((CharacterStatController.TabSkillsId, tabGroup));
|
||||
CharacterStatController.Bind(layout, SampleData.SampleCharacter,
|
||||
spriteResolve: id => (id, 16, 16));
|
||||
|
||||
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
|
||||
|
||||
Assert.Equal("Closed", btn.ActiveState);
|
||||
var sprites = tabGroup.Children.OfType<UiText>().ToList();
|
||||
Assert.Equal(3, sprites.Count);
|
||||
Assert.Equal(0x06005D92u, sprites[0].BackgroundSprite); // Open left-cap
|
||||
Assert.Equal(0x06005D94u, sprites[1].BackgroundSprite); // Open center
|
||||
Assert.Equal(0x06005D96u, sprites[2].BackgroundSprite); // Open right-cap
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TabButtons_TitlesGroupButtons_SetToClosedState()
|
||||
public void TabButtons_SkillsGroup_AddsThreeClosedSpriteChildren()
|
||||
{
|
||||
var tabGroup = new UiPanel();
|
||||
var btn = MakeButton();
|
||||
tabGroup.AddChild(btn);
|
||||
// Skills tab (isOpen=false): three UiText sprite children with Closed sprite ids.
|
||||
var tabGroup = new UiText();
|
||||
var layout = Fake((CharacterStatController.TabSkillsId, tabGroup));
|
||||
|
||||
var layout = Fake((CharacterStatController.TabTitlesId, tabGroup));
|
||||
CharacterStatController.Bind(layout, SampleData.SampleCharacter,
|
||||
spriteResolve: id => (id, 16, 16));
|
||||
|
||||
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
|
||||
var sprites = tabGroup.Children.OfType<UiText>().ToList();
|
||||
Assert.Equal(3, sprites.Count);
|
||||
Assert.Equal(0x06005D93u, sprites[0].BackgroundSprite); // Closed left-cap
|
||||
Assert.Equal(0x06005D95u, sprites[1].BackgroundSprite); // Closed center
|
||||
Assert.Equal(0x06005D97u, sprites[2].BackgroundSprite); // Closed right-cap
|
||||
}
|
||||
|
||||
Assert.Equal("Closed", btn.ActiveState);
|
||||
[Fact]
|
||||
public void TabButtons_TitlesGroup_AddsThreeClosedSpriteChildren()
|
||||
{
|
||||
// Titles tab (isOpen=false): three UiText sprite children with Closed sprite ids.
|
||||
var tabGroup = new UiText();
|
||||
var layout = Fake((CharacterStatController.TabTitlesId, tabGroup));
|
||||
|
||||
CharacterStatController.Bind(layout, SampleData.SampleCharacter,
|
||||
spriteResolve: id => (id, 16, 16));
|
||||
|
||||
var sprites = tabGroup.Children.OfType<UiText>().ToList();
|
||||
Assert.Equal(3, sprites.Count);
|
||||
Assert.Equal(0x06005D93u, sprites[0].BackgroundSprite); // Closed left-cap
|
||||
Assert.Equal(0x06005D95u, sprites[1].BackgroundSprite); // Closed center
|
||||
Assert.Equal(0x06005D97u, sprites[2].BackgroundSprite); // Closed right-cap
|
||||
}
|
||||
|
||||
// ── Affordability helpers (GetRaiseCost) ──────────────────────────────────
|
||||
|
|
@ -644,6 +664,34 @@ public class CharacterStatControllerTests
|
|||
Assert.True(t.RightAligned);
|
||||
}
|
||||
|
||||
// ── Header captions (new — LevelCaptionId + TotalXpLabelId) ─────────────
|
||||
|
||||
[Fact]
|
||||
public void Bind_LevelCaptionId_SetsCharacterLevelText()
|
||||
{
|
||||
var caption = new UiText();
|
||||
var layout = Fake((CharacterStatController.LevelCaptionId, caption));
|
||||
|
||||
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
|
||||
|
||||
Assert.Equal("Character Level", caption.LinesProvider()[0].Text);
|
||||
Assert.False(caption.Centered, "LevelCaption must be left-justified (Centered=false)");
|
||||
Assert.False(caption.RightAligned, "LevelCaption must be left-justified (RightAligned=false)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bind_TotalXpLabelId_SetsTotalExperienceXpText()
|
||||
{
|
||||
var lbl = new UiText();
|
||||
var layout = Fake((CharacterStatController.TotalXpLabelId, lbl));
|
||||
|
||||
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
|
||||
|
||||
Assert.Equal("Total Experience (XP):", lbl.LinesProvider()[0].Text);
|
||||
Assert.False(lbl.Centered, "TotalXpLabel must be left-justified (Centered=false)");
|
||||
Assert.False(lbl.RightAligned, "TotalXpLabel must be left-justified (RightAligned=false)");
|
||||
}
|
||||
|
||||
// ── Robustness ────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue