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

@ -499,6 +499,106 @@ public class DatWidgetFactoryTests
Assert.Equal("42", button.ValueLabel);
}
/// <summary>
/// R4-1 (Campaign CC gate round 1 re-test 3): the value child
/// (<c>0x100002f3</c>) is BASE-INHERITED across four sibling buttons of
/// DIFFERING widths — Health/Stamina/Mana at 150px share the same
/// child id/rect (local X=116) as the wider, 231px Skills credits
/// button — so the child's own authored <c>OriginalParentWidth</c>
/// (baked in at whichever button FIRST resolved it, 150) diverges from
/// the ACTUAL containing button's current width (231) for exactly the
/// wider button. Before this fix, <c>ValueBox</c> was the child's raw
/// (un-reflowed) rect regardless — this fixture reproduces that exact
/// shape (a 150px "design" width baked into the child, hosted under a
/// 231px-wide button, with the child's own Right-tracking edge modes
/// 2/1) and proves <c>UiLayoutPolicy</c>'s raw-edge reflow now shifts
/// the value box by the SAME 81px the button grew (116 -> 197),
/// landing clear of "Available Skill Credits"'s own measured span
/// instead of colliding mid-caption ("Available Skill0Credits").
/// </summary>
[Fact]
public void BuildButton_ValueChildBaseInheritedNarrowerParent_ReflowsToWiderButton()
{
uint captionStringId = 333u;
var info = new ElementInfo { Type = 1, Width = 231, Height = 28 };
info.States[UiStateInfo.DirectStateId] = new UiStateInfo { Id = UiStateInfo.DirectStateId };
info.States[UiStateInfo.DirectStateId].Properties.Values[0x17u] = new UiPropertyValue
{
Kind = UiPropertyKind.StringInfo,
StringInfoValue = new UiStringInfoValue(0, captionStringId, 0, 0, 0, 0),
};
var valueChild = new ElementInfo
{
Type = 12,
X = 116,
Y = 0,
Width = 34,
Height = 28,
Left = 2,
Top = 1,
Right = 1,
Bottom = 1,
OriginalParentWidth = 150,
OriginalParentHeight = 28,
HasOriginalParentSize = true,
HJustify = HJustify.Right,
};
info.Children.Add(valueChild);
var button = Assert.IsType<UiButton>(DatWidgetFactory.Create(
info, NoTex, null,
stringResolve: value => value.StringId == captionStringId ? "Available Skill Credits" : null));
Assert.Equal("Available Skill Credits", button.Label);
// 116 + (231-150) = 197 -> width/height preserved (34/28).
Assert.Equal((197f, 0f, 34f, 28f), button.ValueBox);
Assert.Equal(UiButton.LabelAlignment.Right, button.ValueAlign);
}
/// <summary>
/// Negative companion to the reflow test above: a value child whose
/// OWN authored parent width already MATCHES the actual button (the
/// Health/Stamina/Mana shape, un-widened) reflows to its byte-identical
/// raw rect — delta is zero, so this is confirmed additive, not a
/// blanket shift.
/// </summary>
[Fact]
public void BuildButton_ValueChildOriginalParentMatchesActual_RectUnchanged()
{
uint captionStringId = 334u;
var info = new ElementInfo { Type = 1, Width = 150, Height = 28 };
info.States[UiStateInfo.DirectStateId] = new UiStateInfo { Id = UiStateInfo.DirectStateId };
info.States[UiStateInfo.DirectStateId].Properties.Values[0x17u] = new UiPropertyValue
{
Kind = UiPropertyKind.StringInfo,
StringInfoValue = new UiStringInfoValue(0, captionStringId, 0, 0, 0, 0),
};
var valueChild = new ElementInfo
{
Type = 12,
X = 116,
Y = 0,
Width = 34,
Height = 28,
Left = 2,
Top = 1,
Right = 1,
Bottom = 1,
OriginalParentWidth = 150,
OriginalParentHeight = 28,
HasOriginalParentSize = true,
};
info.Children.Add(valueChild);
var button = Assert.IsType<UiButton>(DatWidgetFactory.Create(
info, NoTex, null,
stringResolve: value => value.StringId == captionStringId ? "Health" : null));
Assert.Equal((116f, 0f, 34f, 28f), button.ValueBox);
}
/// <summary>
/// Negative companion: a button whose caption was LIFTED from a
/// distinct Type-12 child (the town-marker shape,