- Name label color changed from gold to white (retail "Horan" is white; 2026-06-26 ref)
- Level caption now provides 2 lines ("Character" / "Level") so neither truncates in the 65px element
- "Total Experience (XP):" caption (TotalXpLabelId 0x10000234) now visible; fixed Padding=0 on
LabelLeft so the dat-font line is not clipped by the bottom-pin scroll math in small-height elements
- "XP for next level:" caption + value injected as UiText children ON the UiMeter (0x10000236);
the meter's ConsumesDatChildren=true only gates the importer — AddChild at runtime works fine;
framework draws children after OnDraw so the text overlays the red bar faithfully
- Selected footer title (State B) renders WHITE per retail spec; State A title stays body/parchment
- Maxed attribute (raise cost = 0) shows "Infinity!" in Experience To Raise line per retail spec
- 5 new tests; 1 existing LevelCaption test updated to expect 2 lines; 681/683 pass (full suite green)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
BUG 1 (tab bar): Tab group elements (0x10000228/229/538) are UiText with
ConsumesDatChildren=true so their 3 button children are consumed at import.
Fix: inject 3 sprite UiTexts per tab as CHILDREN OF LAYOUT ROOT at absolute
tab rects, ZOrder=8/9 so they draw over dat-imported UiTexts (ZOrder=1-3).
Original tab groups hidden. Active tab (Attributes) gold; inactive parchment.
BUG 2 (footer): Three root causes, all fixed.
(a) _byId stores LAST registered copy per id: stateA (0x10000240) was
the Titles-page copy, hidden by the page-visibility pass. Fixed by
walking root.Children to find the Attributes page (contains NameId)
then FindInSubtree for stateA within that subtree.
(b) Attributes-page stateB/stateC siblings (stacked at y=545) were still
Visible=True, drawing over stateA line-1/line-2. Fixed with
HideAllById walking the Attributes page subtree for ids 241/247.
(c) Footer label elements (H=17-18px, Padding=4f) were routed through
UiTexts scroll path: bottom-pinned baseY ended above the top clip
boundary, silently blanking all text. Fixed: LabelProvider sets
Padding=0f for directly-bound footer single-line labels.
UiDatElement.ElementId exposes _info.Id for subtree id-based walks.
676 tests pass, vitals panel unaffected (regression screenshot clean).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
- UiClickablePanel: new UiPanel subclass with OnClick action + HandlesClick=true
so row clicks survive whole-window-Draggable ancestor frames.
- CharacterStatController overhaul (Pass 2):
- Tab bar button states: SetButtonStateRecursive walks each tab group container
(0x10000228/229/538) setting UiButton.ActiveState="Open" (Attributes) or
"Closed" (Skills/Titles) via UIStateId enum string names from DatReaderWriter.
- Row click: 9 rows become UiClickablePanel; sel[] mutable box drives footer/highlight.
- Toggle: clicking the same row deselects (→ footer State A); click a new row
updates title="Attrib: value", line-1 label="Experience To Raise:", line-1
value=cost, line-2="Unassigned Experience:" in both states.
- Row highlight: BackgroundColor=HighlightBg (semi-translucent gold) on selected row.
- Raise buttons (0x10000246 ×1 + 0x100005EB ×10): hidden initially; shown on
selection with ActiveState="Normal" (affordable) or "Ghosted" (cost=0 or unaffordable).
CollectButtonsById tree-walk finds ALL copies of the button across tab-page mounts
(not just the last-registered _byId copy) so all instances are controlled.
- CharacterSheet: AttributeRaiseCosts long[] (Strength…Mana raise costs in retail
display order; cost=0 → max/disabled row demos the Ghosted button state).
- SampleData.SampleCharacter: fills AttributeRaiseCosts[9] — Strength/Quickness=0
(maxed), Focus@10→110 matching the retail screenshot (spec §4).
- 35 new tests (total 673 pass) covering: row click→footer B title/line1/line2,
toggle deselect→footer A, switch row, highlight set/clear, raise button
hidden/Normal/Ghosted/deselect, tab Open/Closed states, GetRaiseCost helper,
GetRowName helper, SampleData fixture sanity.
Console.WriteLine("[CharacterStat] Row click: index=N → selected=N (Name)") fires
on every click for the user's live verification in the studio.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
Redo of the character pilot per faithful-port rules. The previous pilot GUESSED a text
report and put it on the wrong window. The decomp (verified by dumping acdream's
importer-resolved tree) shows LayoutDesc 0x2100002E is the tabbed Attributes/Skills/Titles
window: its tab-content slot 0x1000022B mounts sub-layout 0x2100002C (gmAttributeUI) which
chains into the gmStatManagementUI header. The importer ALREADY mounts every content element
(FindElement resolves name 0x10000231, heritage 0x10000232, PK 0x10000233, level 0x1000023B,
total-XP 0x10000235, XP meter 0x10000236 → UiMeter, list box 0x1000023D) — EventId was a red
herring (the dat id lives in _byId, not the widget's EventId field).
CharacterStatController (port of gmStatManagementUI::UpdateCharacterInfo 0x004f0770 +
UpdateExperience 0x004f0a70 + UpdatePKStatus 0x004f00a0) binds those real elements: name,
heritage, PK status, level, total XP, the XP-to-level meter fill, and the six innate
attributes in the list box. Studio (--layout 0x2100002E) now renders the actual panel
content, not an overlay. 16 controller tests green; full App suite stays green.
Refinements with known decomp sources (follow-ups): tab-button active/inactive state so the
3 tabs draw their sprites; exact label wording from the StringTable (table 0x10000001 →
dat 0x31000001); the full AttributeInfoRegion row template (column-aligned values + raise
buttons). CharacterController (text-report 0x2100001A) retained for that separate sub-panel.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>