fix(chargen): Campaign CC gate round 1 re-test 3 — R4-1..R4-4

Four visual residuals from the lead's own live-client captures of
1.0.2-cc.m, all root-caused via decomp + live-DAT evidence:

- R4-1: Skills credits value overlapped mid-caption again. Root cause
  was a missing UiLayoutPolicy raw-edge reflow on UiButton's value-child
  rect (the child is base-inherited across four sibling buttons of
  differing widths, so its baked-in OriginalParentWidth diverges from
  the actual 231px-wide Skills credits button) plus an HJustify.Right
  value child mapped to Center instead of a real far-edge Right.
- R4-2: the single-sprite scrollbar thumb tiled (GL_REPEAT) instead of
  drawing once — DrawTiled was reused for a small fixed marker graphic
  whose native size is far smaller than the track-proportional thumb
  rect. New DrawThumbMarker draws exactly one native-size instance.
- R4-3: the skills info-box formula line clipped past the surrounding
  gold frame's own authored bottom edge (the pane's own raw box is 20px
  taller than the frame that visually contains it) — clamp the pane's
  Height to the frame's bottom (register AD-105, since retail's
  ShowSkillsText has no code relationship to the frame to cite).
- R4-4: the Appearance help text started mid-sentence — the box was
  never touched by its page controller, so it kept UiText's chat-style
  PreserveEndOnLayout=true default; the scroll model's wasAtEnd check is
  vacuously true on its first-ever overflow transition, pinning the
  first render to the bottom. Set PreserveEndOnLayout=false (a static
  top-oriented report, not a transcript) and wired the box's own nested
  authored scrollbar, never wired before.

App suite live-DAT env 5372/3 -> 5379/3 (+7, zero regressions). Runtime
1735/0 unchanged. Full solution 14585/4 skips/1 failure (the documented
Core.Net NakEmission full-solution-only flake, confirmed standalone-pass).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-16 19:05:23 +02:00
parent 28704db4bf
commit e6acb800cc
11 changed files with 678 additions and 12 deletions

View file

@ -457,6 +457,37 @@ public sealed class CharacterCreationUiControllerTests
Assert.Equal(VJustify.Top, environment.SkillInfoText().VerticalJustify);
}
/// <summary>
/// R4-3 (Campaign CC gate round 1 re-test 3): the description pane's
/// own raw box (<c>0x100003fc</c>) is TALLER than the surrounding gold
/// decorative frame that visually contains it (<c>0x100003fa</c> —
/// live-DAT-measured, see <see cref="CharacterCreationSkillsPage"/>'s
/// own R4-3 comment for the full geometry + decomp citation: retail's
/// <c>ShowSkillsText</c> has no code relationship between the text
/// panes and this frame, so the frame's own authored bottom edge is
/// the only ground truth for "the visible box"). Before this fix, a
/// long skill's formula line could draw into blank page space below
/// the frame's own border — <c>BuildSkillsPage</c>'s fixture frame
/// (Y=0 H=50) is deliberately shorter than <c>TextInfo</c>'s own
/// default pane Height (60), so this pins the constructor clamping the
/// live-mounted pane's own Height down to the frame's bottom edge
/// (50) instead of leaving it at its own larger raw 60.
/// </summary>
[Fact]
public void SkillsPage_InfoBoxDescriptionPane_HeightClampedToFrameBottom()
{
using var environment = new EnvironmentHarness();
environment.Controller.Open();
environment.Runtime.SelectHeritageDirect(AluvianId);
environment.TabButton(CharacterCreationUiController.SkillsTabElementId).OnClick!();
Assert.Equal(50f, environment.SkillInfoText().Height, 3f);
// The title pane sits OUTSIDE the frame's own child range in this
// fixture (a sibling, not touched by the clamp) — confirms the fix
// is scoped to the description pane only, matching the constructor.
Assert.Equal(60f, environment.SkillInfoTitle().Height, 3f);
}
/// <summary>R2-4a: retail re-selects the row after an arrow click too
/// (<c>ListenToElementMessage @0x004814c0</c>'s own
/// <c>SetSelectedItem(...,1)</c> call following
@ -1229,6 +1260,34 @@ public sealed class CharacterCreationUiControllerTests
environment.Button(CharacterCreationAppearancePage.RotateClockwiseId).OnClick!();
}
/// <summary>
/// R4-4 (Campaign CC gate round 1 re-test 3): the framed help/
/// instructions box (<c>0x100003ab</c>) — before this fix, this
/// element was never touched by <see cref="CharacterCreationAppearancePage"/>'s
/// constructor at all, so it kept <see cref="UiText"/>'s own chat-style
/// default (<c>PreserveEndOnLayout=true</c>) and its own nested
/// authored scrollbar (live-DAT-confirmed a direct Type-11 child,
/// property <c>0x72</c>) was never wired to
/// <see cref="UiText.Scroll"/>. Pins both halves of the fix: the box is
/// no longer chat-style bottom-pinned, and the scrollbar's
/// <see cref="UiScrollbar.Model"/> now points at the SAME
/// <see cref="UiScrollable"/> the text itself scrolls.
/// </summary>
[Fact]
public void AppearancePage_HelpText_TopOriented_AndOwnScrollbarIsWired()
{
using var environment = new EnvironmentHarness();
environment.Controller.Open();
UiText helpText = Assert.IsType<UiText>(
environment.Screen.FindElement(CharacterCreationAppearancePage.HelpTextId));
Assert.False(helpText.PreserveEndOnLayout);
UiScrollbar helpScroll = Assert.IsType<UiScrollbar>(
UiElement.FindDescendant(helpText, 0x100002E7u));
Assert.Same(helpText.Scroll, helpScroll.Model);
}
/// <summary>
/// GF-10 (Campaign CC gate round 1 Batch B): ports
/// <c>gmCGAppearancePage::ZoomIn @0x0047CF00</c>
@ -2813,6 +2872,21 @@ public sealed class CharacterCreationUiControllerTests
page.Children.Add(ScrollbarInfo(0x100003F8u));
page.Children.Add(ButtonInfo(0x100003F9u)); // credits badge
page.Children.Add(TextInfo(0x100003FBu));
// R4-3 (re-test 3): the description pane's own decorative frame
// (0x100003fa, live-DAT-measured SHORTER than the pane it visually
// contains — see CharacterCreationSkillsPage's own R4-3 comment).
// Y=0/Height=50 here is deliberately shorter than TextInfo's own
// default Height=60 so CharacterCreationSkillsPageTests can pin the
// constructor's Height clamp without needing the real installed
// DAT's exact pixel geometry.
page.Children.Add(new ElementInfo
{
Id = 0x100003FAu,
Type = 12u,
Y = 0f,
Width = 200f,
Height = 50f,
});
page.Children.Add(TextInfo(0x100003FCu));
return page;
}
@ -2912,6 +2986,22 @@ public sealed class CharacterCreationUiControllerTests
page.Children.Add(ZoomButtonInfo(CharacterCreationAppearancePage.ZoomInId));
page.Children.Add(ZoomButtonInfo(CharacterCreationAppearancePage.ZoomOutId));
// R4-4 (re-test 3): the framed instructions box, with its OWN
// nested authored scrollbar child — live-DAT-confirmed shape (a
// direct Type-11 child of the Type-12 text box, the SAME nesting
// CharacterCreationSummaryPage's HowToScrollRelativeId already
// uses). Deliberately taller than one view's worth so the long
// static help paragraph genuinely overflows in the test below.
var helpText = TextInfo(CharacterCreationAppearancePage.HelpTextId);
var helpScrollInfo = new ElementInfo { Id = 0x100002E7u, Type = 11u, Width = 12f, Height = 40f };
// UiText/UiField's own dat-children carve-out (LayoutImporter.BuildWidget)
// only builds a child that carries its own authored StateMedia — the
// SAME "genuinely renderable chrome, not swallowed prototype data"
// gate the real scrollbar's own DirectState track sprite satisfies.
helpScrollInfo.StateMedia[""] = (0x06001919u, 1);
helpText.Children.Add(helpScrollInfo);
page.Children.Add(helpText);
return page;
}