fix(chargen): Campaign CC gate round 1 Batch F — Skills page buckets, selection, info box, cost text, arrow states
R2-4/review F1-F2 (gmCGSkillsPage): row click (and arrow click, matching retail's own post-Increase/DecreaseSkillLevel re-select) now selects a skill, highlights its row name, and writes the info panes' title (name + score) and a level-gated bonus line; the description/formula halves stay unported (SkillBase._description/_formula unreachable from this page's current data surface, documented on RefreshInfoBox). The listbox's own authored scrollbar link is wired to its Scroll model (live-DAT-confirmed at 0x100003F8, matching the "+1 from the listbox" hypothesis). Cost text now matches SetSkillText @0x00480600 exactly: Untrained's down-cost and Specialized's up-cost are literal "0", unconditional, where the port previously rendered blank; the 999-blank gate applies to the up-cost only, never to a down-cost. Arrow Ghosted/Enabled state (0x1000001a/ 0x1000001b) is now gated per branch, including bUntrainable/ bUnspecializable re-derived as "this row's own effective cost is nonzero" — no new data needed since the page already resolves that cost. R2-4b (the four-bucket sorted model) is NOT implemented — its Useable- vs-Unuseable-Untrained split reads SkillBase.MinLevel, confirmed present in the installed dat (SkillTable_MinLevelDistribution_NeverExceedsTrained) but not threaded through ChargenOptions/ChargenHeritageOptions/ CharacterCreationRuntimeBindings. AP-213 row records the exact channel a future fix needs. Also live-DAT-pinned: Templates[0]'s header-caption child (0x100002f6) resolves as a UiButton, not UiText, in the real dat — the same UIElement_Button-is-DynamicCast(0xc)-compatible-with-Text quirk already ported for GF-4b's slider labels. App suite (live-DAT env) 5321/3 -> 5328/3 (+7, zero regressions). Runtime 1735/0 unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
2ad805469d
commit
8c30aa18ee
5 changed files with 615 additions and 33 deletions
|
|
@ -1045,6 +1045,66 @@ public sealed class CharacterCreationLiveDatTests
|
|||
Assert.Equal(0x100002F4u, list.Templates[0].TemplateElementId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// R2-4c (Campaign CC gate round 1 Batch F). Live-DAT probe: pins the
|
||||
/// Skills listbox's own authored scrollbar link (dat property
|
||||
/// <c>0x72</c>, <see cref="UiTemplateListBox.ScrollbarElementId"/>) —
|
||||
/// CONFIRMED as <c>0x100003F8</c>, exactly the "+1 from the listbox"
|
||||
/// hypothesis this batch's own investigation raised — and confirms a
|
||||
/// real <see cref="UiScrollbar"/> resolves at that id under the Skills
|
||||
/// page root, the exact fact
|
||||
/// <see cref="CharacterCreationSkillsPage"/>'s constructor now wires
|
||||
/// (<c>scrollbar.Model = list.Scroll</c>).
|
||||
///
|
||||
/// <para>
|
||||
/// Also pins <c>Templates[0]</c>'s (<c>0x100002F4</c>) own header-
|
||||
/// caption child id (<c>0x100002f6</c>, <c>DoSkillRecords @0x00481840</c>'s
|
||||
/// own <c>GetChildRecursive</c> call) as prep evidence for whoever ports
|
||||
/// the four-bucket sorted model (R2-4b, still open — see the AP-213
|
||||
/// row): live-DAT-measured as a <see cref="UiButton"/>, NOT a
|
||||
/// <see cref="UiText"/> — the SAME <c>UIElement_Button</c>-is-
|
||||
/// <c>DynamicCast(0xc)</c>-compatible-with-<c>UIElement_Text</c> quirk
|
||||
/// this campaign already ported for the six attribute-slider labels
|
||||
/// (GF-4b) — retail's own <c>DynamicCast(0xc)</c> cast at
|
||||
/// <c>0x00481855</c> would return null on a REAL Button object
|
||||
/// otherwise, and the very next line unconditionally calls
|
||||
/// <c>UIElement_Text::SetStringInfo</c> on it. A future port reads the
|
||||
/// header caption through <c>UiButton.Label</c>, the same seam GF-4b
|
||||
/// already established.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[InstalledDatFact]
|
||||
public void SkillsPage_Listbox_HasAScrollbarLink_AndHeaderTemplateHasACaptionChild()
|
||||
{
|
||||
using var dats = new DatCollection(DatDirectory, DatAccessType.Read);
|
||||
uint layoutId = RetailDataIdResolver.Resolve(
|
||||
dats, CharacterCreationUiController.RootEnum, 5u);
|
||||
ImportedLayout screen = BuildSelected(
|
||||
dats, layoutId, CharacterCreationUiController.RootElementId);
|
||||
|
||||
UiElement skillsRoot = Assert.IsAssignableFrom<UiElement>(
|
||||
screen.FindElement(CharacterCreationUiController.SkillsPageElementId));
|
||||
UiTemplateListBox list = Assert.IsType<UiTemplateListBox>(
|
||||
UiElement.FindDescendant(skillsRoot, 0x100003F7u));
|
||||
|
||||
Console.WriteLine(
|
||||
$"[CC-Batch-F-DAT] Skills listbox ScrollbarElementId=0x{list.ScrollbarElementId:X8}");
|
||||
Assert.Equal(0x100003F8u, list.ScrollbarElementId);
|
||||
Assert.IsType<UiScrollbar>(
|
||||
UiElement.FindDescendant(skillsRoot, list.ScrollbarElementId));
|
||||
|
||||
UiTemplateListEntry headerTemplate = list.Templates[0];
|
||||
Assert.Equal(0x100002F4u, headerTemplate.TemplateElementId);
|
||||
UiElement? headerRow = LayoutImporter.Import(
|
||||
dats,
|
||||
headerTemplate.TemplateLayoutId,
|
||||
headerTemplate.TemplateElementId,
|
||||
_ => (0u, 0, 0),
|
||||
null)?.Root;
|
||||
UiElement realHeaderRow = Assert.IsAssignableFrom<UiElement>(headerRow);
|
||||
Assert.IsType<UiButton>(UiElement.FindDescendant(realHeaderRow, 0x100002F6u));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GF-15 (Campaign CC gate round 1, Batch A). Live-DAT-probe-confirmed
|
||||
/// during the investigation: the Message dialog catalog's popup
|
||||
|
|
|
|||
|
|
@ -24,6 +24,13 @@ public sealed class CharacterCreationUiControllerTests
|
|||
private const uint SkillTrainOnly = 1u;
|
||||
private const uint SkillSpecializable = 2u;
|
||||
|
||||
/// <summary>Review fix round F2 (Batch F): a "free" skill (trained
|
||||
/// cost 0 in Aluvian's own per-heritage cost list) — pins
|
||||
/// <c>SetSkillText</c>'s <c>bUntrainable</c> re-derivation
|
||||
/// (<c>trainedCost != 0</c>), which locks the down arrow at Trained for
|
||||
/// exactly this shape.</summary>
|
||||
private const uint SkillFreeTrained = 3u;
|
||||
|
||||
[Fact]
|
||||
public void ActiveScreen_KeepsAuthoredRootExtent_AndDefaultsToTheHeritagePage()
|
||||
{
|
||||
|
|
@ -329,8 +336,9 @@ public sealed class CharacterCreationUiControllerTests
|
|||
.OnClick!();
|
||||
|
||||
IReadOnlyList<UiElement> rows = environment.SkillsList().ViewportForTest!.Children;
|
||||
// Aluvian's fixture only costs SkillTrainOnly(1)/SkillSpecializable(2).
|
||||
Assert.Equal(2, rows.Count);
|
||||
// Aluvian's fixture costs SkillTrainOnly(1)/SkillSpecializable(2)/
|
||||
// SkillFreeTrained(3, added for the F2 arrow-lock coverage below).
|
||||
Assert.Equal(3, rows.Count);
|
||||
|
||||
UiElement row = Assert.Single(rows, candidate =>
|
||||
UiElement.FindDescendant(candidate, 0x10000301u) is UiText name
|
||||
|
|
@ -341,11 +349,13 @@ public sealed class CharacterCreationUiControllerTests
|
|||
Assert.Equal((SkillTrainOnly * 10u).ToString(), JoinedText(level));
|
||||
|
||||
// Default (never-touched) level: up cost = trained cost (2), down
|
||||
// cost blank (nothing below Untrained/Inactive).
|
||||
// cost = literal "0" (nothing below Untrained/Inactive, but
|
||||
// SetSkillText @0x00480600's own Untrained branch writes a literal
|
||||
// 0, unconditional — review fix round F1, Batch F).
|
||||
UiText upCost = Assert.IsType<UiText>(UiElement.FindDescendant(row, 0x10000303u));
|
||||
UiText downCost = Assert.IsType<UiText>(UiElement.FindDescendant(row, 0x10000306u));
|
||||
Assert.Equal("2", JoinedText(upCost));
|
||||
Assert.Equal(string.Empty, JoinedText(downCost));
|
||||
Assert.Equal("0", JoinedText(downCost));
|
||||
|
||||
// Advancing to Trained flips the cost pair: up = specCost-trainCost
|
||||
// (6-2=4), down = trainCost (2). FakeRuntime.SetSkillLevel is a
|
||||
|
|
@ -361,6 +371,169 @@ public sealed class CharacterCreationUiControllerTests
|
|||
Assert.Equal("2", JoinedText(downCost));
|
||||
}
|
||||
|
||||
// ── Campaign CC gate round 1 Batch F: R2-4 + review F1/F2 ───────────
|
||||
|
||||
/// <summary>R2-4a: a plain row click selects the skill — the row's own
|
||||
/// NAME text swaps to <c>Vector4.One</c> (the best-derived "brighter
|
||||
/// white") and the info title
|
||||
/// (<c>ShowSkillsText @0x00481250</c>'s <c>" (%d)"</c> score suffix)
|
||||
/// populates. Untrained/Inactive carries no bonus line, so the info
|
||||
/// TEXT pane stays blank (the still-missing description/formula halves
|
||||
/// — see <see cref="CharacterCreationSkillsPage.RefreshInfoBox"/>'s own
|
||||
/// doc).</summary>
|
||||
[Fact]
|
||||
public void SkillsPage_RowClick_SelectsRow_HighlightsNameAndPopulatesInfoBoxTitle()
|
||||
{
|
||||
using var environment = new EnvironmentHarness();
|
||||
environment.Controller.Open();
|
||||
environment.Runtime.SelectHeritageDirect(AluvianId);
|
||||
environment.TabButton(CharacterCreationUiController.SkillsTabElementId).OnClick!();
|
||||
|
||||
(UiDatElement row, UiText nameText) = environment.SkillRow(SkillTrainOnly);
|
||||
Vector4 unselectedColor = nameText.DefaultColor;
|
||||
|
||||
// Nothing selected yet.
|
||||
Assert.Equal(string.Empty, JoinedText(environment.SkillInfoTitle()));
|
||||
|
||||
row.OnClick!();
|
||||
|
||||
Assert.Equal(Vector4.One, nameText.DefaultColor);
|
||||
Assert.NotEqual(unselectedColor, nameText.DefaultColor);
|
||||
|
||||
// FakeRuntime.GetSkillScore's deterministic stand-in: skillId * 10.
|
||||
string expectedTitle =
|
||||
$"{ItemAppraisalTextFormatter.SkillName((int)SkillTrainOnly)} ({SkillTrainOnly * 10u})";
|
||||
Assert.Equal(expectedTitle, JoinedText(environment.SkillInfoTitle()));
|
||||
Assert.Equal(string.Empty, JoinedText(environment.SkillInfoText()));
|
||||
}
|
||||
|
||||
/// <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
|
||||
/// IncreaseSkillLevel/DecreaseSkillLevel) — the info TEXT pane tracks
|
||||
/// the level-gated bonus line as the skill advances (the TWO-space
|
||||
/// literal <c>"Training Bonus +5"</c>/<c>"Specialization Bonus +10"</c>,
|
||||
/// matching the compiled string verbatim).</summary>
|
||||
[Fact]
|
||||
public void SkillsPage_ArrowClick_AlsoSelectsRow_InfoBoxShowsLevelBonusLine()
|
||||
{
|
||||
using var environment = new EnvironmentHarness();
|
||||
environment.Controller.Open();
|
||||
environment.Runtime.SelectHeritageDirect(AluvianId);
|
||||
environment.TabButton(CharacterCreationUiController.SkillsTabElementId).OnClick!();
|
||||
|
||||
(UiButton up, _) = environment.SkillRowArrows(SkillTrainOnly);
|
||||
|
||||
up.OnClick!(); // Untrained/Inactive -> Trained.
|
||||
BumpRevisionAndTick(environment);
|
||||
Assert.Equal("Training Bonus +5", JoinedText(environment.SkillInfoText()));
|
||||
|
||||
up.OnClick!(); // Trained -> Specialized.
|
||||
BumpRevisionAndTick(environment);
|
||||
Assert.Equal("Specialization Bonus +10", JoinedText(environment.SkillInfoText()));
|
||||
}
|
||||
|
||||
/// <summary>R2-4a: selecting a SECOND row restores the FIRST row's own
|
||||
/// authored (unselected) color instead of leaving it stuck
|
||||
/// highlighted.</summary>
|
||||
[Fact]
|
||||
public void SkillsPage_RowClick_DeselectsPreviousRow_RestoresItsOwnColor()
|
||||
{
|
||||
using var environment = new EnvironmentHarness();
|
||||
environment.Controller.Open();
|
||||
environment.Runtime.SelectHeritageDirect(AluvianId);
|
||||
environment.TabButton(CharacterCreationUiController.SkillsTabElementId).OnClick!();
|
||||
|
||||
(UiDatElement firstRow, UiText firstName) = environment.SkillRow(SkillTrainOnly);
|
||||
Vector4 firstUnselected = firstName.DefaultColor;
|
||||
(UiDatElement secondRow, UiText secondName) = environment.SkillRow(SkillSpecializable);
|
||||
|
||||
firstRow.OnClick!();
|
||||
Assert.Equal(Vector4.One, firstName.DefaultColor);
|
||||
|
||||
secondRow.OnClick!();
|
||||
Assert.Equal(Vector4.One, secondName.DefaultColor);
|
||||
Assert.Equal(firstUnselected, firstName.DefaultColor);
|
||||
}
|
||||
|
||||
/// <summary>Review fix round F1: <c>SetSkillText</c>'s Specialized
|
||||
/// branch (<c>@0x00480679</c>) writes a literal <c>"0"</c> up-cost,
|
||||
/// unconditional (nothing above Specialized needs the 999-blank gate),
|
||||
/// and an UNCONDITIONAL down-cost — no gate even though this fixture's
|
||||
/// value (4) happens to be well under 999.</summary>
|
||||
[Fact]
|
||||
public void SkillsPage_SpecializedCostText_UpCostIsLiteralZero_DownCostUnconditional()
|
||||
{
|
||||
using var environment = new EnvironmentHarness();
|
||||
environment.Controller.Open();
|
||||
environment.Runtime.SelectHeritageDirect(AluvianId);
|
||||
environment.TabButton(CharacterCreationUiController.SkillsTabElementId).OnClick!();
|
||||
|
||||
environment.Runtime.View.SetSkillLevel(SkillSpecializable, ChargenSkillAdvancementClass.Specialized);
|
||||
BumpRevisionAndTick(environment);
|
||||
|
||||
(UiElement row, _) = environment.SkillRow(SkillSpecializable);
|
||||
UiText upCost = Assert.IsType<UiText>(UiElement.FindDescendant(row, 0x10000303u));
|
||||
UiText downCost = Assert.IsType<UiText>(UiElement.FindDescendant(row, 0x10000306u));
|
||||
|
||||
Assert.Equal("0", JoinedText(upCost));
|
||||
Assert.Equal("4", JoinedText(downCost)); // specCost(6) - trainCost(2).
|
||||
}
|
||||
|
||||
/// <summary>Review fix round F2: the Up/Down arrow Ghosted
|
||||
/// (<c>0x1000001a</c>)/Enabled (<c>0x1000001b</c>) state pair —
|
||||
/// Untrained's Down is ALWAYS ghosted (nothing below it); Up is gated
|
||||
/// on <c>remainingSkillCredits</c>; a "free" skill (trained cost 0)
|
||||
/// locks its OWN down arrow at Trained
|
||||
/// (<c>bUntrainable</c> re-derived as <c>trainedCost != 0</c>) and
|
||||
/// unlocks it again once Specialized (specialized cost is non-zero),
|
||||
/// where its own Up arrow is then ALWAYS ghosted (nothing above
|
||||
/// Specialized).</summary>
|
||||
[Fact]
|
||||
public void SkillsPage_ArrowStates_GatedOnCreditsAndFreeSkillLocksDownArrow()
|
||||
{
|
||||
using var environment = new EnvironmentHarness();
|
||||
environment.Controller.Open();
|
||||
environment.Runtime.SelectHeritageDirect(AluvianId);
|
||||
environment.TabButton(CharacterCreationUiController.SkillsTabElementId).OnClick!();
|
||||
|
||||
// Untrained/Inactive: Down ALWAYS ghosted; Up enabled (credits 50
|
||||
// >= trainedCost 2).
|
||||
(UiButton up, UiButton down) = environment.SkillRowArrows(SkillTrainOnly);
|
||||
Assert.Equal(0x1000001Bu, up.ActiveRetailStateId);
|
||||
Assert.Equal(0x1000001Au, down.ActiveRetailStateId);
|
||||
|
||||
up.OnClick!(); // -> Trained. trainedCost(2) != 0 -> Down enabled.
|
||||
BumpRevisionAndTick(environment);
|
||||
Assert.Equal(0x1000001Bu, down.ActiveRetailStateId);
|
||||
|
||||
(UiButton freeUp, UiButton freeDown) = environment.SkillRowArrows(SkillFreeTrained);
|
||||
freeUp.OnClick!(); // -> Trained. trainedCost(0) == 0 -> Down locked.
|
||||
BumpRevisionAndTick(environment);
|
||||
Assert.Equal(0x1000001Au, freeDown.ActiveRetailStateId);
|
||||
|
||||
freeUp.OnClick!(); // -> Specialized. specCost(6) != 0 -> Down unlocks;
|
||||
BumpRevisionAndTick(environment); // Up is now ALWAYS ghosted.
|
||||
Assert.Equal(0x1000001Au, freeUp.ActiveRetailStateId);
|
||||
Assert.Equal(0x1000001Bu, freeDown.ActiveRetailStateId);
|
||||
}
|
||||
|
||||
/// <summary>R2-4c: the listbox's own authored scrollbar link
|
||||
/// (<see cref="UiTemplateListBox.ScrollbarElementId"/>) is wired to the
|
||||
/// SAME listbox's <see cref="UiTemplateListBox.Scroll"/> model — the
|
||||
/// ordinary page-level <c>UiScrollbar.Model</c> linkage, no widget
|
||||
/// change.</summary>
|
||||
[Fact]
|
||||
public void SkillsPage_ListboxScrollbar_IsLinkedToTheListsOwnScroll()
|
||||
{
|
||||
using var environment = new EnvironmentHarness();
|
||||
environment.Controller.Open();
|
||||
environment.TabButton(CharacterCreationUiController.SkillsTabElementId).OnClick!();
|
||||
|
||||
UiScrollbar scrollbar = environment.SkillsScrollbar();
|
||||
Assert.Same(environment.SkillsList().Scroll, scrollbar.Model);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TownButton_SelectsTheLiteralStartAreaIndex()
|
||||
{
|
||||
|
|
@ -1804,6 +1977,33 @@ public sealed class CharacterCreationUiControllerTests
|
|||
return (up, down);
|
||||
}
|
||||
|
||||
/// <summary>R2-4a (Batch F): locates a built skill row's ROOT
|
||||
/// element (<c>Templates[1]</c>'s own <c>UiDatElement</c>) by its
|
||||
/// name text, for row-CLICK (not arrow-click) selection tests.
|
||||
/// Also returns the row's own name <see cref="UiText"/> so a test
|
||||
/// can assert its <see cref="UiText.DefaultColor"/> selection
|
||||
/// highlight.</summary>
|
||||
public (UiDatElement Row, UiText NameText) SkillRow(uint skillId)
|
||||
{
|
||||
string skillName = ItemAppraisalTextFormatter.SkillName((int)skillId);
|
||||
UiElement row = Assert.Single(
|
||||
SkillsList().ViewportForTest!.Children,
|
||||
candidate => UiElement.FindDescendant(candidate, 0x10000301u) is UiText name
|
||||
&& JoinedText(name) == skillName);
|
||||
UiDatElement datRow = Assert.IsType<UiDatElement>(row);
|
||||
UiText nameText = Assert.IsType<UiText>(UiElement.FindDescendant(row, 0x10000301u));
|
||||
return (datRow, nameText);
|
||||
}
|
||||
|
||||
public UiText SkillInfoTitle() =>
|
||||
Assert.IsType<UiText>(Screen.FindElement(0x100003FBu));
|
||||
|
||||
public UiText SkillInfoText() =>
|
||||
Assert.IsType<UiText>(Screen.FindElement(0x100003FCu));
|
||||
|
||||
public UiScrollbar SkillsScrollbar() =>
|
||||
Assert.IsType<UiScrollbar>(Screen.FindElement(0x100003F8u));
|
||||
|
||||
public UiTemplateListBox SummaryListBox() =>
|
||||
Assert.IsType<UiTemplateListBox>(Screen.FindElement(CharacterCreationSummaryPage.ListBoxId));
|
||||
|
||||
|
|
@ -2171,6 +2371,7 @@ public sealed class CharacterCreationUiControllerTests
|
|||
{
|
||||
[SkillTrainOnly] = new(SkillTrainOnly, NormalCost: 2, PrimaryCost: 6),
|
||||
[SkillSpecializable] = new(SkillSpecializable, NormalCost: 2, PrimaryCost: 6),
|
||||
[SkillFreeTrained] = new(SkillFreeTrained, NormalCost: 0, PrimaryCost: 6),
|
||||
};
|
||||
|
||||
var aluvian = new ChargenHeritageOptions(
|
||||
|
|
@ -2406,6 +2607,12 @@ public sealed class CharacterCreationUiControllerTests
|
|||
Y = 40f,
|
||||
Width = 300f,
|
||||
Height = 320f,
|
||||
// R2-4c (Batch F): the listbox's own authored scrollbar link
|
||||
// (dat property 0x72) — an arbitrary but plausible sibling id
|
||||
// (retail's own "+1 from the listbox" convention, matching the
|
||||
// hypothesis this batch's own investigation raised) since no
|
||||
// live-DAT probe has pinned the real installed value yet.
|
||||
ScrollbarElementId = 0x100003F8u,
|
||||
};
|
||||
// GF-5 (2026-08-16): [0] is retail's own bucket-HEADER row
|
||||
// (0x100002F4, unused by this port's flat-list simplification);
|
||||
|
|
@ -2415,6 +2622,7 @@ public sealed class CharacterCreationUiControllerTests
|
|||
list.TemplateList.Add(new UiTemplateListEntry(0x21000038u, 0x100002F4u));
|
||||
list.TemplateList.Add(new UiTemplateListEntry(0x21000038u, 0x100002FFu));
|
||||
page.Children.Add(list);
|
||||
page.Children.Add(ScrollbarInfo(0x100003F8u));
|
||||
page.Children.Add(ButtonInfo(0x100003F9u)); // credits badge
|
||||
page.Children.Add(TextInfo(0x100003FBu));
|
||||
page.Children.Add(TextInfo(0x100003FCu));
|
||||
|
|
@ -2552,6 +2760,24 @@ public sealed class CharacterCreationUiControllerTests
|
|||
/// a bare button shape since this port's flat-list simplification never
|
||||
/// resolves it.
|
||||
/// </summary>
|
||||
/// <summary>Review fix round F2 (Batch F): retail's own custom
|
||||
/// Ghosted(<c>0x1000001a</c>)/Enabled(<c>0x1000001b</c>) state pair for
|
||||
/// <c>pSkillUpButton</c>/<c>pSkillDownButton</c> — mirrors
|
||||
/// <c>CharacterCreationSkillsPage</c>'s own (private)
|
||||
/// <c>ArrowGhostedStateId</c>/<c>ArrowEnabledStateId</c> consts so
|
||||
/// <c>TrySetRetailState</c> has real, matching state descriptors to
|
||||
/// resolve against (the raw-numeric-id lookup path,
|
||||
/// <c>UiButton.TryFindState</c>) — the SAME "author arbitrary numeric
|
||||
/// states directly" pattern <see cref="BuildAppearancePage"/> already
|
||||
/// uses for its own custom pair.</summary>
|
||||
private static ElementInfo ArrowButtonInfo(uint id)
|
||||
{
|
||||
ElementInfo info = ButtonInfo(id);
|
||||
info.States[0x1000001Au] = new UiStateInfo { Id = 0x1000001Au, Name = "ArrowGhosted" };
|
||||
info.States[0x1000001Bu] = new UiStateInfo { Id = 0x1000001Bu, Name = "ArrowEnabled" };
|
||||
return info;
|
||||
}
|
||||
|
||||
private static UiElement BuildSkillRowTemplate(uint templateElementId)
|
||||
{
|
||||
if (templateElementId == 0x100002FFu)
|
||||
|
|
@ -2563,12 +2789,19 @@ public sealed class CharacterCreationUiControllerTests
|
|||
Width = 280f,
|
||||
Height = 16f,
|
||||
};
|
||||
// R2-4a (Batch F): an explicit, distinguishable unselected
|
||||
// color (retail's own gold list-caption tone, the same
|
||||
// 218,167,85 GF-11b measured) so selection tests can tell
|
||||
// CharacterCreationSkillsPage.SelectedNameColor (pure white)
|
||||
// apart from a row's own authored default.
|
||||
ElementInfo nameInfo = TextInfo(0x10000301u);
|
||||
nameInfo.FontColor = new System.Numerics.Vector4(218f / 255f, 167f / 255f, 85f / 255f, 1f);
|
||||
row.Children.Add(ContainerInfo(0x10000300u)); // unreferenced icon/backdrop
|
||||
row.Children.Add(TextInfo(0x10000301u)); // name
|
||||
row.Children.Add(nameInfo); // name
|
||||
row.Children.Add(TextInfo(0x10000302u)); // pSkillLevelText
|
||||
row.Children.Add(TextInfo(0x10000303u)); // pUpCostText
|
||||
row.Children.Add(ButtonInfo(0x10000304u)); // pSkillUpButton
|
||||
row.Children.Add(ButtonInfo(0x10000305u)); // pSkillDownButton
|
||||
row.Children.Add(ArrowButtonInfo(0x10000304u)); // pSkillUpButton
|
||||
row.Children.Add(ArrowButtonInfo(0x10000305u)); // pSkillDownButton
|
||||
row.Children.Add(TextInfo(0x10000306u)); // pDownCostText
|
||||
return LayoutImporter.Build(row, _ => (0u, 0, 0), null).Root;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue