feat(ui): Campaign CT slice CT3 — Titles page live via standard GUI classes
Titles tab (AP-109's known-inert gap) now switches to a real page and CharacterTitlesController binds it entirely through UiTemplateListBox/ UiScrollbar/UiButton — zero bespoke widgets, matching every other social/options row-list page in this codebase. Retail anchors: gmCharacterTitleUI::PostInit @0x0049A610; AddTitleToList @0x0049A840 + FindSortedInsertPosition @0x0049A760 (rows sorted by resolved display text — this port rebuilds the full sorted set on every change rather than a positional splice, since UiTemplateListBox has no insert-at-index primitive and no other consumer needs one either); InfoRegion::SetState(selected?6:1) (row Highlight/DirectState swap, the same mechanism CT1's SEALED VERDICT confirmed for the stat rows); UpdateButtons @0x0049A500 CORRECTED direction (Ghosted unless a row is selected whose id differs from the current display title — no selection IS the Ghosted case); Refresh @0x0049abc0 (display-title text, including the hardcoded "Unknown" fallback, refreshed on both TableReplaced and DisplayTitleChanged per CT2's review anchor 1); Event_SetDisplayCharacterTitle @0x006a5720 (wire-only TitleSet 0x002C send, no local mutation). CharacterStatController.Bind now three-way switches Attributes/Skills/ Titles — Titles is a genuinely separate, non-duplicated page container (CT1 ground truth §3), unlike Attributes/Skills which share one mounted page and only rebind content. The two page captions (0x1000052E/0x10000531) are left untouched: LayoutImporter.BuildText already resolves every element's authored StringInfo caption at import time, so no controller-side string lookup was added. New IGameRuntimeCommands.SetTitle seam on DeferredGameRuntimeStateCommands (InteractionUiRuntimeSources.cs) mirrors the existing Advance() shape. CharacterRuntimeBindings gains Titles/TitleResolver/SendSetTitle; CharacterTitleResolver (CT2) is constructed once at composition time and its .Resolve method group is passed to the controller as a delegate (not the concrete DAT-backed type) so the controller stays hermetically testable without a live IDatReaderWriter. Tests (tests/AcDream.App.Tests/UI/Layout/CharacterTitlesControllerTests.cs): binding-seam coverage against the REAL committed character_2100002E.json fixture (verified this session to already carry the Titles page subtree, including the ListBox's own authored TemplateList=[(0x2100005E, 0x10000536)] entry — RowTemplateResolver_ReceivesTheFixturesOwnAuthoredTemplateIds proves the controller reads that authored pair, not a hardcoded one); a hand-authored ElementInfo standing in only for the row template itself (a separate LayoutDesc with no committed fixture yet — CT1 was a live-DAT probe only); sorted-row order, Unknown fallback, row selection/highlight, the ghost truth table (no selection / selected==display / selected!= display), click-sends-exactly-one-SetTitle-and-mutates-nothing, click-while-ghosted-sends-nothing, TableReplaced rebuild (including selection survival when the id is still earned), TitleAdded single-row growth, DisplayTitleChanged text+ghost refresh, and Dispose unsubscription. CharacterStatControllerTests updated for the Titles tab no longer being ClickThrough, plus a new tab-switch visibility test. Register: amends AP-109 (docs/architecture/retail-divergence-register.md) to record the Titles-page half as LIVE; the header identity block and luminance fields remain open for CT4. Suites: full solution 15,405 tests / 0 skips (App 6,130) green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
b5d36f5211
commit
03e073b748
8 changed files with 913 additions and 14 deletions
|
|
@ -802,7 +802,10 @@ public class CharacterStatControllerTests
|
|||
Assert.Equal(
|
||||
new uint[] { 0x06005D93u, 0x06005D95u, 0x06005D97u },
|
||||
skills.Children.Cast<UiDatElement>().Select(child => child.ActiveMedia().File));
|
||||
Assert.True(titles.ClickThrough);
|
||||
// CT3 (2026-08-24): the Titles tab is no longer inert — it switches
|
||||
// to a real page (previously AP-109's known gap).
|
||||
Assert.False(titles.ClickThrough);
|
||||
Assert.NotNull(titles.OnClick);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -826,6 +829,45 @@ public class CharacterStatControllerTests
|
|||
Assert.Equal(RetailUiStateIds.Open, Assert.IsAssignableFrom<IUiDatStateful>(child).ActiveRetailStateId));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CT3 (2026-08-24): unlike Attributes/Skills (which share ONE mounted
|
||||
/// page and only rebind its content), the Titles page (0x10000539) is a
|
||||
/// genuinely separate, non-duplicated subtree (CT1 ground truth §3) that
|
||||
/// must actually be shown/hidden on tab switch.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void TitlesTab_Click_ShowsTitlesPageAndHidesAttributesSkillsContent()
|
||||
{
|
||||
var layout = FixtureLoader.LoadCharacter();
|
||||
var titlesTab = Assert.IsType<UiText>(layout.FindElement(CharacterStatController.TabTitlesId));
|
||||
var attributesTab = Assert.IsType<UiText>(layout.FindElement(CharacterStatController.TabAttribId));
|
||||
var titlesPage = layout.FindElement(CharacterStatController.TitlesPageId);
|
||||
var attributesPage = layout.FindElement(CharacterStatController.AttributesPageId);
|
||||
Assert.NotNull(titlesPage);
|
||||
Assert.NotNull(attributesPage);
|
||||
|
||||
CharacterStatController.Bind(layout, SampleData.SampleCharacter,
|
||||
spriteResolve: id => (id, 16, 16));
|
||||
|
||||
Assert.True(attributesPage!.Visible);
|
||||
Assert.False(titlesPage!.Visible);
|
||||
|
||||
titlesTab.OnClick!();
|
||||
|
||||
Assert.True(titlesPage.Visible);
|
||||
Assert.False(attributesPage.Visible);
|
||||
Assert.Equal(RetailUiStateIds.Open, titlesTab.ActiveRetailStateId);
|
||||
Assert.Equal(RetailUiStateIds.Closed, attributesTab.ActiveRetailStateId);
|
||||
|
||||
// Switching back to Attributes restores the shared content page and
|
||||
// re-hides Titles.
|
||||
attributesTab.OnClick!();
|
||||
|
||||
Assert.True(attributesPage.Visible);
|
||||
Assert.False(titlesPage.Visible);
|
||||
Assert.Equal(RetailUiStateIds.Closed, titlesTab.ActiveRetailStateId);
|
||||
}
|
||||
|
||||
// ── Affordability helpers (GetRaiseCost) ──────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue