fix(D.2b): Attributes tab — fill panel height, center row icons, footer at bottom

Root cause: the sub-layout 0x2100002C (design H=337px) mounted into tab slot
0x1000022B (H=575px) via ShouldMountBaseChildren. ElementReader.Merge takes the
derived (sub-layout) H=337 as canonical, so the background element's first
ApplyAnchor call captured _amB=238 and the list box captured _amB=65 — both
stayed at their 337px-parent sizes even though the slot is 575px.

Fix: CascadeHeight in LayoutImporter.Resolve mirrors retail's
UIElement::UpdateForParentSizeChange. When ShouldMountBaseChildren fires and the
slot is taller than the base design, every full-stretch background child has its
height cascaded: Top+Bottom anchors → stretch, Bottom-only → pin-to-bottom,
Top-only/None → unchanged. This propagates the correct bottom margins through the
entire subtree before the first render frame.

Layout result (confirmed via headless screenshot):
- List box grows from 160px → 398px (9 rows × 44px ≈ 396px)
- Footer elements move from abs Y≈307 → abs Y≈545 (matching retail dump)
- Separator moves to abs Y≈535

Row constants updated: RowHeight 44px, IconSize 24px, RowPadX 4px, IconGap 6px.
Footer State-A text corrected per spec: title="Select an Attribute to Improve",
line-1 label="Skill Credits Available:", line-1 value=SkillCredits (96),
line-2 label="Unassigned Experience:", line-2 value=UnassignedXp (formatted N0).
CharacterSheet.UnassignedXp (long, retail InqInt64(2)) added.
SampleData.SampleCharacter().UnassignedXp = 87_757_321_741L.
5 footer tests renamed + assertions updated; SampleCharacter_UnassignedXp_IsSet added.
639 tests green.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-25 21:19:45 +02:00
parent 902160098a
commit eccacc59de
6 changed files with 178 additions and 50 deletions

View file

@ -212,62 +212,65 @@ public class CharacterStatControllerTests
// ── Footer State A ────────────────────────────────────────────────────────
[Fact]
public void Bind_FooterStateA_TitleEmpty()
public void Bind_FooterStateA_TitleIsSelectPrompt()
{
var title = new UiText();
var layout = Fake((CharacterStatController.FooterTitleId, title));
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
Assert.Equal(string.Empty, title.LinesProvider()[0].Text);
Assert.Equal("Select an Attribute to Improve", title.LinesProvider()[0].Text);
}
[Fact]
public void Bind_FooterStateA_Line1LabelEmpty()
public void Bind_FooterStateA_Line1LabelIsSkillCreditsAvailable()
{
var lbl = new UiText();
var layout = Fake((CharacterStatController.FooterLine1Label, lbl));
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
Assert.Equal(string.Empty, lbl.LinesProvider()[0].Text);
Assert.Equal("Skill Credits Available:", lbl.LinesProvider()[0].Text);
}
[Fact]
public void Bind_FooterStateA_Line1ValueIsSelectPrompt()
public void Bind_FooterStateA_Line1ValueIsSkillCredits()
{
var val = new UiText();
var layout = Fake((CharacterStatController.FooterLine1Value, val));
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
Assert.Equal("Select an Attribute to Improve", val.LinesProvider()[0].Text);
}
[Fact]
public void Bind_FooterStateA_Line2LabelEmpty()
{
var lbl = new UiText();
var layout = Fake((CharacterStatController.FooterLine2Label, lbl));
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
Assert.Equal(string.Empty, lbl.LinesProvider()[0].Text);
}
[Fact]
public void Bind_FooterStateA_Line2ValueIsSkillCredits()
{
var val = new UiText();
var layout = Fake((CharacterStatController.FooterLine2Value, val));
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
// SampleData.SampleCharacter().SkillCredits == 96
Assert.Equal("96", val.LinesProvider()[0].Text);
}
// ── SkillCredits propagation through SampleData ───────────────────────────
[Fact]
public void Bind_FooterStateA_Line2LabelIsUnassignedExperience()
{
var lbl = new UiText();
var layout = Fake((CharacterStatController.FooterLine2Label, lbl));
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
Assert.Equal("Unassigned Experience:", lbl.LinesProvider()[0].Text);
}
[Fact]
public void Bind_FooterStateA_Line2ValueIsUnassignedXp()
{
var val = new UiText();
var layout = Fake((CharacterStatController.FooterLine2Value, val));
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
// SampleData.SampleCharacter().UnassignedXp == 87_757_321_741
// Formatted as "N0" with thousands separators.
var expected = (87_757_321_741L).ToString("N0");
Assert.Equal(expected, val.LinesProvider()[0].Text);
}
// ── SkillCredits / UnassignedXp propagation through SampleData ───────────
[Fact]
public void SampleCharacter_SkillCredits_Is96()
@ -275,6 +278,12 @@ public class CharacterStatControllerTests
Assert.Equal(96, SampleData.SampleCharacter().SkillCredits);
}
[Fact]
public void SampleCharacter_UnassignedXp_IsSet()
{
Assert.Equal(87_757_321_741L, SampleData.SampleCharacter().UnassignedXp);
}
// ── UiText.RightAligned — unit-level draw mode flag ──────────────────────
[Fact]