fix(chargen): Campaign CC gate round 1 Batch C — rich text + labels + backdrops
Commit 1/3: chargen-scoped, low blast-radius fixes. - New DatRichText helper: escape-normalize + word-wrap + per-segment palette color, porting UIElement_Text::SetStringInfoWithFont / AppendStringInfoWithFont's composition model. Routes the Heritage (GF-2), Town (GF-11a), and Profession (GF-3) description boxes through it instead of a raw unwrapped single-Line LinesProvider. Heritage headers use font-color palette index 1 (green), bodies index 0 (white), matching AppendStringInfoWithFont's own font-index argument. Town's diagnosed GF-11a root cause: a single un-wrapped line meant the town-specific suffix rendered past the clipped viewport, so switching towns looked like "text never changes" even though the underlying composed string genuinely differed. - GF-3: bind the Profession page's description textbox (0x100003e0, gmCGProfessionPage::InitializePage @0x00483068) and compose its per-template text (UpdateProfession @0x004821b0's CustomText/ BowText/SwashText/LifeText/WarText/WayText/SoldierText, plain SetStringInfo — no palette). - GF-4: UiButton gains a coexisting ValueLabel/ValueBox/ValueFont/ ValueColor slot alongside Label. Retail's chargen display buttons (avail/health/stamina/mana credits, 0x100003e2-e5/0x100003f9) author their caption directly on P0x17 AND carry a separate, media-less Type-12 value child that UiButton.ConsumesDatChildren used to drop entirely — pages substituted the button's own Label, destroying the caption. DatWidgetFactory.BuildButton now surfaces that child (gated on ReferenceEquals(labelInfo, info) — own-caption buttons only) instead. The six Profession slider name labels (0x100002ed, CharGenState::GetAttributeName @0x005C3A20's six hardcoded literals) resolve as UiButton in this port (live-DAT- measured Type 1 — retail's UIElement_Button is DynamicCast(0xc)- compatible with UIElement_Text) and are written once at construction, matching retail's own single InitializePage write. - GF-6/AP-218: gmCGAppearancePage::Update writes a heritage-flavored STATIC caption to the Hair/Eyes/Skin spins (plain / GearText_* / OlthoiText_* variants) — never an index. Removed the prior 1-based- ordinal/gear-name substitution entirely; the other six spins keep their DAT-authored caption untouched, matching retail exactly. - Root 1d: wire the Heritage (0x100003be, 13 states) and Profession (0x100003d8, 7 states) backdrop SetState cascades (gmCGHeritagePage::Update / gmCGProfessionPage::UpdateProfession). - AP-216/AP-217 (partial, register updated honestly): swatches beyond the current part's real color count now hide (DoColorSpots' blank- blit half); the GradCircle now blanks for Eyes (DoGradDisk's blank- plug half). The "paint with the actual represented/current color" halves stay open — they need a PalSet/Palette-id -> RGB pipeline no chargen page reads at runtime yet, judged disproportionate to add alongside this batch's other ~10 fixes. Register: AP-215 rewritten (item 2's "ordinal" framing is stale after GF-6; restated as the icon-thumbnail gap), AP-216/AP-217 rewritten (partially closed), AP-218 retired, AD-103 retired (the swallowed- child Label substitution AD-103 tracked is replaced by ValueLabel's own-geometry surfacing). 22 new tests (DatRichText unit tests, UiButton/DatWidgetFactory ValueLabel tests, live-DAT structural pins, controller behavioral tests) — all green. Full App suite (Release, live-DAT): 5300 passed / 1 pre-existing unrelated flake (PortalProjectionTests allocation test, passes in isolation) / 3 skipped, up from the baseline 5282/3. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
7d09821fdc
commit
0591b9a026
13 changed files with 1018 additions and 66 deletions
|
|
@ -1505,6 +1505,164 @@ public sealed class CharacterCreationUiControllerTests
|
|||
public void RotateCounterClockwise() => RotateCounterClockwiseCalls++;
|
||||
}
|
||||
|
||||
// ── Campaign CC gate round 1 Batch C ────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// GF-2: the composed description routes through the shared rich-text
|
||||
/// helper — header segments (palette index 1) render in a DIFFERENT
|
||||
/// color than body segments (index 0), and each segment's own escape
|
||||
/// sequence is normalized. The fixture's description element carries
|
||||
/// no authored <c>FontColorPalette</c>, so this also exercises
|
||||
/// <see cref="DatRichText.PaletteColor"/>'s fallback (green header /
|
||||
/// white body).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void HeritageDescription_ComposesGreenHeaderAndWhiteBodySegments()
|
||||
{
|
||||
using var environment = new EnvironmentHarness();
|
||||
environment.Runtime.ResolvedStrings["ID_CharGen_Heritage_StartingSkills_Header"] = "Trained Starting Skills:";
|
||||
environment.Runtime.ResolvedStrings["ID_CharGen_Heritage_StartingSkills"] = "Line one\\nLine two";
|
||||
environment.Controller.Open();
|
||||
environment.Runtime.SelectHeritageDirect(AluvianId);
|
||||
BumpRevisionAndTick(environment);
|
||||
|
||||
UiText description = Assert.IsType<UiText>(environment.Screen.FindElement(0x100003C4u));
|
||||
var lines = description.LinesProvider().ToList();
|
||||
|
||||
Assert.Contains(lines, l => l.Text == "Trained Starting Skills:" && l.Color == new Vector4(0f, 1f, 0f, 1f));
|
||||
// The literal "\n" escape in the body segment must become TWO
|
||||
// separate lines, not render as a literal backslash-n.
|
||||
Assert.Contains(lines, l => l.Text == "Line one" && l.Color == Vector4.One);
|
||||
Assert.Contains(lines, l => l.Text == "Line two" && l.Color == Vector4.One);
|
||||
Assert.DoesNotContain(lines, l => l.Text.Contains("\\n"));
|
||||
}
|
||||
|
||||
/// <summary>GF-11a: switching towns changes the RENDERED (wrapped)
|
||||
/// lines, not just an internal string that never becomes visible —
|
||||
/// the diagnosed root cause of "description does not change" was a
|
||||
/// single un-wrapped line whose differing suffix rendered past the
|
||||
/// clipped viewport.</summary>
|
||||
[Fact]
|
||||
public void TownDescription_ChangesRenderedLinesWhenSwitchingTowns()
|
||||
{
|
||||
using var environment = new EnvironmentHarness();
|
||||
environment.Runtime.ResolvedStrings["ID_CharGen_TownHowTo"] = "How to pick a town.";
|
||||
environment.Runtime.ResolvedStrings["ID_CharGen_HoltText"] = "Holtburg is snowy.";
|
||||
environment.Runtime.ResolvedStrings["ID_CharGen_ShoushiText"] = "Shoushi is sunny.";
|
||||
environment.Controller.Open();
|
||||
environment.TabButton(CharacterCreationUiController.TownTabElementId).OnClick!();
|
||||
|
||||
environment.Button(0x1000040Du).OnClick!(); // Holtburg
|
||||
BumpRevisionAndTick(environment);
|
||||
UiText description = Assert.IsType<UiText>(environment.Screen.FindElement(0x10000409u));
|
||||
string holtburgText = JoinedText(description);
|
||||
Assert.Contains("Holtburg is snowy.", holtburgText);
|
||||
|
||||
environment.Button(0x1000040Fu).OnClick!(); // Shoushi
|
||||
BumpRevisionAndTick(environment);
|
||||
string shoushiText = JoinedText(description);
|
||||
Assert.Contains("Shoushi is sunny.", shoushiText);
|
||||
Assert.DoesNotContain("Holtburg is snowy.", shoushiText);
|
||||
}
|
||||
|
||||
/// <summary>GF-3: the Profession page's description textbox
|
||||
/// (<c>0x100003e0</c>) binds and switches per selected template.</summary>
|
||||
[Fact]
|
||||
public void ProfessionDescription_BindsAndSwitchesPerTemplate()
|
||||
{
|
||||
using var environment = new EnvironmentHarness();
|
||||
environment.Runtime.ResolvedStrings["ID_CharGen_CustomText"] = "Custom flexible build.";
|
||||
environment.Runtime.ResolvedStrings["ID_CharGen_BowText"] = "Bow hunters use ranged attacks.";
|
||||
environment.Controller.Open();
|
||||
environment.Runtime.SelectHeritageDirect(AluvianId);
|
||||
environment.TabButton(CharacterCreationUiController.ProfessionTabElementId).OnClick!();
|
||||
|
||||
environment.Button(0x100003DAu).OnClick!(); // Bow Hunter = template 1
|
||||
BumpRevisionAndTick(environment);
|
||||
|
||||
UiText description = Assert.IsType<UiText>(environment.Screen.FindElement(0x100003E0u));
|
||||
Assert.Contains("Bow hunters use ranged attacks.", JoinedText(description));
|
||||
}
|
||||
|
||||
/// <summary>GF-4a: the display buttons' authored caption survives a
|
||||
/// value write — the whole point of the ValueLabel coexistence
|
||||
/// mechanism.</summary>
|
||||
[Fact]
|
||||
public void ProfessionAndSkillsDisplayButtons_ValueWriteDoesNotClobberLabel()
|
||||
{
|
||||
using var environment = new EnvironmentHarness();
|
||||
environment.Controller.Open();
|
||||
environment.Runtime.SelectHeritageDirect(AluvianId);
|
||||
|
||||
UiButton available = environment.Button(0x100003E2u);
|
||||
available.Label = "Attribute Credits"; // fixture authors no P0x17; simulate it
|
||||
UiButton credits = environment.Button(0x100003F9u);
|
||||
credits.Label = "Available Skill Credits";
|
||||
|
||||
environment.TabButton(CharacterCreationUiController.ProfessionTabElementId).OnClick!();
|
||||
BumpRevisionAndTick(environment);
|
||||
Assert.Equal("Attribute Credits", available.Label);
|
||||
// The fixture's default snapshot (BuildOptions' companion default)
|
||||
// carries RemainingAttributeCredits=66 — an exact, non-vacuous
|
||||
// pin, not just "some value got written somewhere".
|
||||
Assert.Equal("66", available.ValueLabel);
|
||||
|
||||
environment.TabButton(CharacterCreationUiController.SkillsTabElementId).OnClick!();
|
||||
BumpRevisionAndTick(environment);
|
||||
Assert.Equal("Available Skill Credits", credits.Label);
|
||||
Assert.Equal("50", credits.ValueLabel); // RemainingSkillCredits=50
|
||||
}
|
||||
|
||||
/// <summary>GF-6/AP-218: the Appearance page's Hair/Eyes/Skin spins
|
||||
/// show a heritage-flavored STATIC caption, never a numeric ordinal —
|
||||
/// and switch to the Gearknight/Olthoi variant per heritage.</summary>
|
||||
[Fact]
|
||||
public void AppearanceSpinCaptions_ArePartNames_NotOrdinals_AndVaryByHeritage()
|
||||
{
|
||||
using var environment = new EnvironmentHarness();
|
||||
environment.Runtime.ResolvedStrings["ID_CharGen_HairStyle"] = "Hair Style";
|
||||
environment.Runtime.ResolvedStrings["ID_CharGen_Eyes"] = "Eyes";
|
||||
environment.Runtime.ResolvedStrings["ID_CharGen_GearText_HairButton"] = "Gear Hair";
|
||||
environment.Controller.Open();
|
||||
environment.Runtime.SelectHeritageDirect(AluvianId);
|
||||
environment.TabButton(CharacterCreationUiController.AppearanceTabElementId).OnClick!();
|
||||
BumpRevisionAndTick(environment);
|
||||
|
||||
UiButton hairSpin = environment.Button(CharacterCreationAppearancePage.HairSpinId);
|
||||
Assert.Equal("Hair Style", hairSpin.Label);
|
||||
Assert.DoesNotContain(hairSpin.Label, new[] { "1", "2", "-" });
|
||||
|
||||
environment.Runtime.SelectHeritageDirect((uint)ChargenHeritageGroup.Gearknight);
|
||||
BumpRevisionAndTick(environment);
|
||||
Assert.Equal("Gear Hair", hairSpin.Label);
|
||||
}
|
||||
|
||||
/// <summary>Root 1d: the Heritage and Profession backdrops switch
|
||||
/// state per selection.</summary>
|
||||
[Fact]
|
||||
public void HeritageAndProfessionBackdrops_SwitchStatePerSelection()
|
||||
{
|
||||
using var environment = new EnvironmentHarness();
|
||||
environment.Controller.Open();
|
||||
|
||||
var heritageBackdrop = Assert.IsAssignableFrom<IUiDatStateful>(
|
||||
environment.Screen.FindElement(0x100003BEu));
|
||||
environment.Runtime.SelectHeritageDirect(AluvianId);
|
||||
BumpRevisionAndTick(environment);
|
||||
Assert.Equal(0x10000021u, heritageBackdrop.ActiveRetailStateId);
|
||||
|
||||
environment.Runtime.SelectHeritageDirect((uint)ChargenHeritageGroup.Gharundim);
|
||||
BumpRevisionAndTick(environment);
|
||||
Assert.Equal(0x10000022u, heritageBackdrop.ActiveRetailStateId);
|
||||
|
||||
environment.TabButton(CharacterCreationUiController.ProfessionTabElementId).OnClick!();
|
||||
var professionBackdrop = Assert.IsAssignableFrom<IUiDatStateful>(
|
||||
environment.Screen.FindElement(0x100003D8u));
|
||||
environment.Button(0x100003DAu).OnClick!(); // Bow Hunter = template 1
|
||||
BumpRevisionAndTick(environment);
|
||||
Assert.Equal(0x1000002Cu, professionBackdrop.ActiveRetailStateId);
|
||||
}
|
||||
|
||||
private static void BumpRevisionAndTick(EnvironmentHarness environment)
|
||||
{
|
||||
RuntimeCharacterCreationSnapshot snapshot = environment.Runtime.View.Snapshot;
|
||||
|
|
@ -2113,6 +2271,20 @@ public sealed class CharacterCreationUiControllerTests
|
|||
page.Children.Add(ButtonInfo(0x100005C7u)); // Olthoi
|
||||
page.Children.Add(ButtonInfo(0x100005F1u)); // Lugian (F3 quirk: no tab-restore/hide)
|
||||
page.Children.Add(TextInfo(0x100003C4u));
|
||||
|
||||
// Root 1d: the backdrop, live-DAT-measured 13 authored states
|
||||
// (see CharacterCreationHeritagePage.BackdropStateByHeritage).
|
||||
var backdrop = ContainerInfo(0x100003BEu);
|
||||
foreach (uint stateId in new[]
|
||||
{
|
||||
0x10000021u, 0x10000022u, 0x10000023u, 0x10000024u, 0x10000058u,
|
||||
0x10000059u, 0x1000005Au, 0x1000005Bu, 0x1000005Cu, 0x1000005Du,
|
||||
0x1000005Eu, 0x1000005Fu, 0x10000060u,
|
||||
})
|
||||
{
|
||||
backdrop.States[stateId] = new UiStateInfo { Id = stateId, Name = $"State_{stateId:X8}" };
|
||||
}
|
||||
page.Children.Add(backdrop);
|
||||
return page;
|
||||
}
|
||||
|
||||
|
|
@ -2138,6 +2310,21 @@ public sealed class CharacterCreationUiControllerTests
|
|||
page.Children.Add(ButtonInfo(0x100003E3u)); // Health
|
||||
page.Children.Add(ButtonInfo(0x100003E4u)); // Stamina
|
||||
page.Children.Add(ButtonInfo(0x100003E5u)); // Mana
|
||||
|
||||
page.Children.Add(TextInfo(0x100003E0u)); // GF-3: description textbox
|
||||
|
||||
// Root 1d: the backdrop, live-DAT-measured 7 authored states (see
|
||||
// CharacterCreationProfessionPage.BackdropStateByTemplate).
|
||||
var backdrop = ContainerInfo(0x100003D8u);
|
||||
foreach (uint stateId in new[]
|
||||
{
|
||||
0x1000002Bu, 0x1000002Cu, 0x1000002Du,
|
||||
0x1000002Eu, 0x1000002Fu, 0x10000030u, 0x10000031u,
|
||||
})
|
||||
{
|
||||
backdrop.States[stateId] = new UiStateInfo { Id = stateId, Name = $"State_{stateId:X8}" };
|
||||
}
|
||||
page.Children.Add(backdrop);
|
||||
return page;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue