fix(studio): Character window — bind the REAL Attributes-tab elements (no guessing)

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>
This commit is contained in:
Erik 2026-06-25 19:47:33 +02:00
parent 33693c6412
commit 0e644b5887
5 changed files with 250 additions and 2 deletions

View file

@ -33,6 +33,25 @@ public sealed class CharacterSheet
/// <summary>Title string, e.g. "the Adventurer". Null = omit.</summary>
public string? Title { get; init; }
// ── Experience / PK (gmStatManagementUI::UpdateExperience 0x004f0a70,
// UpdatePKStatus 0x004f00a0) — the Attributes-tab header strip ──────────
/// <summary>Total accrued experience (retail PropertyInt64 1). Header value
/// element 0x10000235 (m_pTotalXPText).</summary>
public long TotalXp { get; init; }
/// <summary>Experience remaining to the next level. Header value element
/// 0x10000238 (m_pXPToLevelText).</summary>
public long XpToNextLevel { get; init; }
/// <summary>XP-to-next-level meter fill, 0..1 (retail (curbase)/(capbase)).
/// Drives the header meter 0x10000236 (m_pXPToLevelMeter).</summary>
public float XpFraction { get; init; }
/// <summary>PK status display string, e.g. "Non-Player Killer". Header element
/// 0x10000233 (m_pPKStatusText). Null = omit.</summary>
public string? PkStatus { get; init; }
// ── Birth / age / deaths (UpdatePlayerBirthAgeDeaths 0x004b8cb0) ─────────
/// <summary>Formatted birth date string (retail InqInt(0x62) → strftime).