feat(studio): Attributes tab Pass 2 — click-to-select (highlight + footer B + raise triangles) + tab states
- UiClickablePanel: new UiPanel subclass with OnClick action + HandlesClick=true
so row clicks survive whole-window-Draggable ancestor frames.
- CharacterStatController overhaul (Pass 2):
- Tab bar button states: SetButtonStateRecursive walks each tab group container
(0x10000228/229/538) setting UiButton.ActiveState="Open" (Attributes) or
"Closed" (Skills/Titles) via UIStateId enum string names from DatReaderWriter.
- Row click: 9 rows become UiClickablePanel; sel[] mutable box drives footer/highlight.
- Toggle: clicking the same row deselects (→ footer State A); click a new row
updates title="Attrib: value", line-1 label="Experience To Raise:", line-1
value=cost, line-2="Unassigned Experience:" in both states.
- Row highlight: BackgroundColor=HighlightBg (semi-translucent gold) on selected row.
- Raise buttons (0x10000246 ×1 + 0x100005EB ×10): hidden initially; shown on
selection with ActiveState="Normal" (affordable) or "Ghosted" (cost=0 or unaffordable).
CollectButtonsById tree-walk finds ALL copies of the button across tab-page mounts
(not just the last-registered _byId copy) so all instances are controlled.
- CharacterSheet: AttributeRaiseCosts long[] (Strength…Mana raise costs in retail
display order; cost=0 → max/disabled row demos the Ghosted button state).
- SampleData.SampleCharacter: fills AttributeRaiseCosts[9] — Strength/Quickness=0
(maxed), Focus@10→110 matching the retail screenshot (spec §4).
- 35 new tests (total 673 pass) covering: row click→footer B title/line1/line2,
toggle deselect→footer A, switch row, highlight set/clear, raise button
hidden/Normal/Ghosted/deselect, tab Open/Closed states, GetRaiseCost helper,
GetRowName helper, SampleData fixture sanity.
Console.WriteLine("[CharacterStat] Row click: index=N → selected=N (Name)") fires
on every click for the user's live verification in the studio.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
21d8485053
commit
defbde1f86
5 changed files with 817 additions and 145 deletions
|
|
@ -8,6 +8,10 @@ namespace AcDream.App.Tests.UI.Layout;
|
|||
/// Unit tests for <see cref="CharacterStatController"/> — the Attributes-tab controller that
|
||||
/// binds the REAL importer-mounted header + 9-row list + footer elements (no created overlay).
|
||||
/// Pure data wiring against fake layouts; no dats, no GL.
|
||||
///
|
||||
/// <para>Pass 1 tests verify header/XP meter/attribute list/footer State-A binding.
|
||||
/// Pass 2 tests verify: (a) row click → selection toggle; (b) footer State-B content;
|
||||
/// (c) raise-button affordability; (d) tab button states.</para>
|
||||
/// </summary>
|
||||
public class CharacterStatControllerTests
|
||||
{
|
||||
|
|
@ -66,7 +70,6 @@ public class CharacterStatControllerTests
|
|||
|
||||
// ── Attribute list — 9 rows in list box 0x1000023D ───────────────────────
|
||||
|
||||
/// <summary>Bind adds exactly 9 UiPanel row containers to the list box.</summary>
|
||||
[Fact]
|
||||
public void Bind_AttributeList_Has9Rows()
|
||||
{
|
||||
|
|
@ -75,14 +78,29 @@ public class CharacterStatControllerTests
|
|||
|
||||
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
|
||||
|
||||
// Rows are UiClickablePanel which inherits UiPanel, so OfType<UiPanel> matches.
|
||||
var rows = list.Children.OfType<UiPanel>().ToList();
|
||||
Assert.Equal(9, rows.Count);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Every row must have a right-aligned value UiText child (the last UiText in
|
||||
/// the row), confirming the RightAligned path was wired.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Bind_AttributeList_RowsAreClickablePanels()
|
||||
{
|
||||
var list = new UiPanel();
|
||||
var layout = Fake((CharacterStatController.ListBoxId, list));
|
||||
|
||||
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
|
||||
|
||||
var rows = list.Children.OfType<UiClickablePanel>().ToList();
|
||||
Assert.Equal(9, rows.Count);
|
||||
// All rows must have an OnClick wired (not null) and ClickThrough = false.
|
||||
foreach (var row in rows)
|
||||
{
|
||||
Assert.NotNull(row.OnClick);
|
||||
Assert.False(row.ClickThrough, "clickable row must accept pointer hits");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bind_AttributeList_EachRowHasRightAlignedValueLabel()
|
||||
{
|
||||
|
|
@ -95,18 +113,13 @@ public class CharacterStatControllerTests
|
|||
Assert.Equal(9, rows.Count);
|
||||
foreach (var row in rows)
|
||||
{
|
||||
// Last UiText in each row is the value (right-aligned).
|
||||
var texts = row.Children.OfType<UiText>().ToList();
|
||||
Assert.True(texts.Count >= 2, "each row must have at least name + value UiText");
|
||||
var valueEl = texts[^1]; // last = value
|
||||
var valueEl = texts[^1];
|
||||
Assert.True(valueEl.RightAligned, "value label must be RightAligned");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail display order: Strength, Endurance, Coordination, Quickness, Focus, Self,
|
||||
/// Health, Stamina, Mana (spec §1). Name is the SECOND UiText child of each row.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Bind_AttributeList_RowNamesInRetailOrder()
|
||||
{
|
||||
|
|
@ -126,19 +139,13 @@ public class CharacterStatControllerTests
|
|||
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
// Name UiText = second UiText in the row (after the icon UiText).
|
||||
var texts = rows[i].Children.OfType<UiText>().ToList();
|
||||
Assert.True(texts.Count >= 2, $"row {i} must have name + value");
|
||||
string rowName = texts[1].LinesProvider()[0].Text; // index 1 = name (0 = icon)
|
||||
string rowName = texts[1].LinesProvider()[0].Text;
|
||||
Assert.Equal(expectedNames[i], rowName);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attribute row values are plain integers; vital row values are "cur/max".
|
||||
/// Validates against SampleData fixture: Str=200, Quick=200, rest=10;
|
||||
/// Health=5/5, Stamina=10/10, Mana=10/10.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Bind_AttributeList_RowValues_AttributeIntegersAndVitalsCurMax()
|
||||
{
|
||||
|
|
@ -149,45 +156,34 @@ public class CharacterStatControllerTests
|
|||
|
||||
var rows = list.Children.OfType<UiPanel>().ToList();
|
||||
|
||||
// Helper: value text is the last UiText in the row.
|
||||
string ValueOf(UiPanel row) => row.Children.OfType<UiText>().ToList()[^1].LinesProvider()[0].Text;
|
||||
|
||||
// Attribute rows 0-5.
|
||||
Assert.Equal("200", ValueOf(rows[0])); // Strength
|
||||
Assert.Equal("10", ValueOf(rows[1])); // Endurance
|
||||
Assert.Equal("10", ValueOf(rows[2])); // Coordination
|
||||
Assert.Equal("200", ValueOf(rows[3])); // Quickness
|
||||
Assert.Equal("10", ValueOf(rows[4])); // Focus
|
||||
Assert.Equal("10", ValueOf(rows[5])); // Self
|
||||
|
||||
// Vital rows 6-8: "cur/max" format.
|
||||
Assert.Equal("5/5", ValueOf(rows[6])); // Health
|
||||
Assert.Equal("10/10", ValueOf(rows[7])); // Stamina
|
||||
Assert.Equal("10/10", ValueOf(rows[8])); // Mana
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When spriteResolve is provided the icon UiText must have BackgroundSprite set
|
||||
/// to the correct DataID; when null the BackgroundSprite must be 0.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Bind_AttributeList_IconHasBackgroundSpriteWhenResolverProvided()
|
||||
{
|
||||
var list = new UiPanel();
|
||||
var layout = Fake((CharacterStatController.ListBoxId, list));
|
||||
|
||||
// Non-null resolver — just returns a fake handle for any id.
|
||||
CharacterStatController.Bind(layout, SampleData.SampleCharacter,
|
||||
spriteResolve: id => (id, 16, 16)); // fake: handle == id
|
||||
spriteResolve: id => (id, 16, 16));
|
||||
|
||||
var rows = list.Children.OfType<UiPanel>().ToList();
|
||||
Assert.Equal(9, rows.Count);
|
||||
|
||||
// First icon = Strength row; expected DataID 0x060002C8.
|
||||
var iconEl = rows[0].Children.OfType<UiText>().First();
|
||||
Assert.Equal(0x060002C8u, iconEl.BackgroundSprite);
|
||||
|
||||
// Vital (Health) = row 6; expected DataID 0x06004C3B.
|
||||
var healthIcon = rows[6].Children.OfType<UiText>().First();
|
||||
Assert.Equal(0x06004C3Bu, healthIcon.BackgroundSprite);
|
||||
}
|
||||
|
|
@ -219,6 +215,7 @@ public class CharacterStatControllerTests
|
|||
|
||||
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
|
||||
|
||||
// Initial state (nothing selected): State-A title.
|
||||
Assert.Equal("Select an Attribute to Improve", title.LinesProvider()[0].Text);
|
||||
}
|
||||
|
||||
|
|
@ -241,7 +238,6 @@ public class CharacterStatControllerTests
|
|||
|
||||
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
|
||||
|
||||
// SampleData.SampleCharacter().SkillCredits == 96
|
||||
Assert.Equal("96", val.LinesProvider()[0].Text);
|
||||
}
|
||||
|
||||
|
|
@ -264,27 +260,375 @@ public class CharacterStatControllerTests
|
|||
|
||||
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
|
||||
|
||||
// SampleData.SampleCharacter().UnassignedXp == 87_757_321_741
|
||||
// Formatted as "N0" with thousands separators.
|
||||
var expected = (87_757_321_741L).ToString("N0");
|
||||
Assert.Equal(expected, val.LinesProvider()[0].Text);
|
||||
}
|
||||
|
||||
// ── SkillCredits / UnassignedXp propagation through SampleData ───────────
|
||||
// ── Pass 2: Row selection → Footer State B ───────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void RowClick_SelectRow4Focus_FooterStateBShowsFocusTitle()
|
||||
{
|
||||
// Focus is index 4 in AttrRows. SampleData Focus = 10. Cost = 110.
|
||||
var title = new UiText();
|
||||
var list = new UiPanel();
|
||||
var layout = Fake(
|
||||
(CharacterStatController.FooterTitleId, title),
|
||||
(CharacterStatController.ListBoxId, list));
|
||||
|
||||
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
|
||||
|
||||
// Simulate a click on row 4 (Focus).
|
||||
var rows = list.Children.OfType<UiClickablePanel>().ToList();
|
||||
Assert.Equal(9, rows.Count);
|
||||
rows[4].OnClick!();
|
||||
|
||||
// Footer title should now be "Focus: 10".
|
||||
Assert.Equal("Focus: 10", title.LinesProvider()[0].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RowClick_SelectRow4Focus_FooterLine1LabelIsExperienceToRaise()
|
||||
{
|
||||
var lbl = new UiText();
|
||||
var list = new UiPanel();
|
||||
var layout = Fake(
|
||||
(CharacterStatController.FooterLine1Label, lbl),
|
||||
(CharacterStatController.ListBoxId, list));
|
||||
|
||||
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
|
||||
list.Children.OfType<UiClickablePanel>().ToList()[4].OnClick!();
|
||||
|
||||
Assert.Equal("Experience To Raise:", lbl.LinesProvider()[0].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RowClick_SelectRow4Focus_FooterLine1ValueIsRaiseCost()
|
||||
{
|
||||
var val = new UiText();
|
||||
var list = new UiPanel();
|
||||
var layout = Fake(
|
||||
(CharacterStatController.FooterLine1Value, val),
|
||||
(CharacterStatController.ListBoxId, list));
|
||||
|
||||
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
|
||||
list.Children.OfType<UiClickablePanel>().ToList()[4].OnClick!();
|
||||
|
||||
// Focus raise cost = 110 (SampleData fixture).
|
||||
Assert.Equal((110L).ToString("N0"), val.LinesProvider()[0].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RowClick_SelectRow4Focus_FooterLine2LabelIsUnassignedExperience()
|
||||
{
|
||||
var lbl = new UiText();
|
||||
var list = new UiPanel();
|
||||
var layout = Fake(
|
||||
(CharacterStatController.FooterLine2Label, lbl),
|
||||
(CharacterStatController.ListBoxId, list));
|
||||
|
||||
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
|
||||
list.Children.OfType<UiClickablePanel>().ToList()[4].OnClick!();
|
||||
|
||||
Assert.Equal("Unassigned Experience:", lbl.LinesProvider()[0].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RowClick_SelectRow4Focus_FooterLine2ValueIsUnassignedXp()
|
||||
{
|
||||
var val = new UiText();
|
||||
var list = new UiPanel();
|
||||
var layout = Fake(
|
||||
(CharacterStatController.FooterLine2Value, val),
|
||||
(CharacterStatController.ListBoxId, list));
|
||||
|
||||
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
|
||||
list.Children.OfType<UiClickablePanel>().ToList()[4].OnClick!();
|
||||
|
||||
// UnassignedXp = 87_757_321_741L
|
||||
var expected = (87_757_321_741L).ToString("N0");
|
||||
Assert.Equal(expected, val.LinesProvider()[0].Text);
|
||||
}
|
||||
|
||||
// ── Pass 2: Toggle deselects ──────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void RowClick_ToggleSameRow_ReturnsToFooterStateA()
|
||||
{
|
||||
var title = new UiText();
|
||||
var list = new UiPanel();
|
||||
var layout = Fake(
|
||||
(CharacterStatController.FooterTitleId, title),
|
||||
(CharacterStatController.ListBoxId, list));
|
||||
|
||||
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
|
||||
|
||||
var rows = list.Children.OfType<UiClickablePanel>().ToList();
|
||||
|
||||
// Select Focus (row 4).
|
||||
rows[4].OnClick!();
|
||||
Assert.Equal("Focus: 10", title.LinesProvider()[0].Text);
|
||||
|
||||
// Click the same row again → deselect.
|
||||
rows[4].OnClick!();
|
||||
Assert.Equal("Select an Attribute to Improve", title.LinesProvider()[0].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RowClick_SwitchRow_UpdatesToNewRow()
|
||||
{
|
||||
var title = new UiText();
|
||||
var list = new UiPanel();
|
||||
var layout = Fake(
|
||||
(CharacterStatController.FooterTitleId, title),
|
||||
(CharacterStatController.ListBoxId, list));
|
||||
|
||||
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
|
||||
|
||||
var rows = list.Children.OfType<UiClickablePanel>().ToList();
|
||||
|
||||
// Select Endurance (row 1, value=10).
|
||||
rows[1].OnClick!();
|
||||
Assert.Equal("Endurance: 10", title.LinesProvider()[0].Text);
|
||||
|
||||
// Select Self (row 5, value=10).
|
||||
rows[5].OnClick!();
|
||||
Assert.Equal("Self: 10", title.LinesProvider()[0].Text);
|
||||
}
|
||||
|
||||
// ── Pass 2: Row highlight ─────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void RowClick_SelectRow_HighlightsSelectedAndClearsOthers()
|
||||
{
|
||||
var list = new UiPanel();
|
||||
var layout = Fake((CharacterStatController.ListBoxId, list));
|
||||
|
||||
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
|
||||
|
||||
var rows = list.Children.OfType<UiClickablePanel>().ToList();
|
||||
|
||||
// All rows start transparent.
|
||||
Assert.All(rows, r => Assert.Equal(0f, r.BackgroundColor.W));
|
||||
|
||||
// Select row 2 (Coordination).
|
||||
rows[2].OnClick!();
|
||||
Assert.NotEqual(0f, rows[2].BackgroundColor.W); // highlighted
|
||||
Assert.Equal(0f, rows[0].BackgroundColor.W); // others cleared
|
||||
Assert.Equal(0f, rows[1].BackgroundColor.W);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RowClick_Deselect_ClearsHighlight()
|
||||
{
|
||||
var list = new UiPanel();
|
||||
var layout = Fake((CharacterStatController.ListBoxId, list));
|
||||
|
||||
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
|
||||
|
||||
var rows = list.Children.OfType<UiClickablePanel>().ToList();
|
||||
rows[2].OnClick!(); // select
|
||||
rows[2].OnClick!(); // deselect
|
||||
|
||||
Assert.Equal(0f, rows[2].BackgroundColor.W);
|
||||
}
|
||||
|
||||
// ── Pass 2: Raise button affordability ───────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void RaiseButtons_InitiallyHidden()
|
||||
{
|
||||
var btn1 = MakeButton();
|
||||
var btn10 = MakeButton();
|
||||
var list = new UiPanel();
|
||||
var layout = Fake(
|
||||
(CharacterStatController.RaiseOneId, btn1),
|
||||
(CharacterStatController.RaiseTenId, btn10),
|
||||
(CharacterStatController.ListBoxId, list));
|
||||
|
||||
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
|
||||
|
||||
Assert.False(btn1.Visible, "raise×1 must start hidden");
|
||||
Assert.False(btn10.Visible, "raise×10 must start hidden");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RaiseButtons_AffordableRow_ShowsNormalState()
|
||||
{
|
||||
// Focus row (index 4) cost=110, UnassignedXp=87_757_321_741 → affordable.
|
||||
var btn1 = MakeButton();
|
||||
var btn10 = MakeButton();
|
||||
var list = new UiPanel();
|
||||
var layout = Fake(
|
||||
(CharacterStatController.RaiseOneId, btn1),
|
||||
(CharacterStatController.RaiseTenId, btn10),
|
||||
(CharacterStatController.ListBoxId, list));
|
||||
|
||||
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
|
||||
|
||||
list.Children.OfType<UiClickablePanel>().ToList()[4].OnClick!(); // select Focus
|
||||
|
||||
Assert.True(btn1.Visible, "raise×1 visible on selection");
|
||||
Assert.True(btn10.Visible, "raise×10 visible on selection");
|
||||
Assert.Equal("Normal", btn1.ActiveState);
|
||||
Assert.Equal("Normal", btn10.ActiveState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RaiseButtons_MaxedRow_ShowsGhostedState()
|
||||
{
|
||||
// Strength row (index 0) cost=0 → disabled. UnassignedXp is irrelevant.
|
||||
var btn1 = MakeButton();
|
||||
var btn10 = MakeButton();
|
||||
var list = new UiPanel();
|
||||
var layout = Fake(
|
||||
(CharacterStatController.RaiseOneId, btn1),
|
||||
(CharacterStatController.RaiseTenId, btn10),
|
||||
(CharacterStatController.ListBoxId, list));
|
||||
|
||||
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
|
||||
|
||||
list.Children.OfType<UiClickablePanel>().ToList()[0].OnClick!(); // select Strength (cost=0)
|
||||
|
||||
Assert.True(btn1.Visible, "raise button visible even when disabled");
|
||||
Assert.Equal("Ghosted", btn1.ActiveState);
|
||||
Assert.Equal("Ghosted", btn10.ActiveState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RaiseButtons_Deselect_HidesButtons()
|
||||
{
|
||||
var btn1 = MakeButton();
|
||||
var list = new UiPanel();
|
||||
var layout = Fake(
|
||||
(CharacterStatController.RaiseOneId, btn1),
|
||||
(CharacterStatController.ListBoxId, list));
|
||||
|
||||
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
|
||||
|
||||
var rows = list.Children.OfType<UiClickablePanel>().ToList();
|
||||
rows[4].OnClick!(); // select
|
||||
Assert.True(btn1.Visible);
|
||||
rows[4].OnClick!(); // deselect
|
||||
Assert.False(btn1.Visible, "raise button hidden after deselect");
|
||||
}
|
||||
|
||||
// ── Pass 2: Tab button states ─────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void TabButtons_AttributesGroupButtons_SetToOpenState()
|
||||
{
|
||||
var tabGroup = new UiPanel(); // fake tab group container
|
||||
var btn1 = MakeButton();
|
||||
var btn2 = MakeButton();
|
||||
tabGroup.AddChild(btn1);
|
||||
tabGroup.AddChild(btn2);
|
||||
|
||||
var layout = Fake((CharacterStatController.TabAttribId, tabGroup));
|
||||
|
||||
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
|
||||
|
||||
// All UiButton children of the Attributes group should be "Open".
|
||||
Assert.Equal("Open", btn1.ActiveState);
|
||||
Assert.Equal("Open", btn2.ActiveState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TabButtons_SkillsGroupButtons_SetToClosedState()
|
||||
{
|
||||
var tabGroup = new UiPanel();
|
||||
var btn = MakeButton();
|
||||
tabGroup.AddChild(btn);
|
||||
|
||||
var layout = Fake((CharacterStatController.TabSkillsId, tabGroup));
|
||||
|
||||
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
|
||||
|
||||
Assert.Equal("Closed", btn.ActiveState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TabButtons_TitlesGroupButtons_SetToClosedState()
|
||||
{
|
||||
var tabGroup = new UiPanel();
|
||||
var btn = MakeButton();
|
||||
tabGroup.AddChild(btn);
|
||||
|
||||
var layout = Fake((CharacterStatController.TabTitlesId, tabGroup));
|
||||
|
||||
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
|
||||
|
||||
Assert.Equal("Closed", btn.ActiveState);
|
||||
}
|
||||
|
||||
// ── Affordability helpers (GetRaiseCost) ──────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void GetRaiseCost_Index4Focus_Returns110()
|
||||
{
|
||||
var sheet = SampleData.SampleCharacter();
|
||||
Assert.Equal(110L, CharacterStatController.GetRaiseCost(sheet, 4));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetRaiseCost_Index0Strength_Returns0()
|
||||
{
|
||||
var sheet = SampleData.SampleCharacter();
|
||||
Assert.Equal(0L, CharacterStatController.GetRaiseCost(sheet, 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetRaiseCost_OutOfRange_Returns0()
|
||||
{
|
||||
var sheet = SampleData.SampleCharacter();
|
||||
Assert.Equal(0L, CharacterStatController.GetRaiseCost(sheet, 99));
|
||||
}
|
||||
|
||||
// ── GetRowName helper ─────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void GetRowName_Index0_ReturnsStrength()
|
||||
=> Assert.Equal("Strength", CharacterStatController.GetRowName(0));
|
||||
|
||||
[Fact]
|
||||
public void GetRowName_Index4_ReturnsFocus()
|
||||
=> Assert.Equal("Focus", CharacterStatController.GetRowName(4));
|
||||
|
||||
[Fact]
|
||||
public void GetRowName_Index6_ReturnsHealth()
|
||||
=> Assert.Equal("Health", CharacterStatController.GetRowName(6));
|
||||
|
||||
[Fact]
|
||||
public void GetRowName_NegativeIndex_ReturnsEmpty()
|
||||
=> Assert.Equal(string.Empty, CharacterStatController.GetRowName(-1));
|
||||
|
||||
// ── SampleData sanity ─────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void SampleCharacter_SkillCredits_Is96()
|
||||
{
|
||||
Assert.Equal(96, SampleData.SampleCharacter().SkillCredits);
|
||||
}
|
||||
=> Assert.Equal(96, SampleData.SampleCharacter().SkillCredits);
|
||||
|
||||
[Fact]
|
||||
public void SampleCharacter_UnassignedXp_IsSet()
|
||||
=> Assert.Equal(87_757_321_741L, SampleData.SampleCharacter().UnassignedXp);
|
||||
|
||||
[Fact]
|
||||
public void SampleCharacter_AttributeRaiseCosts_HasNineEntries()
|
||||
{
|
||||
Assert.Equal(87_757_321_741L, SampleData.SampleCharacter().UnassignedXp);
|
||||
var costs = SampleData.SampleCharacter().AttributeRaiseCosts;
|
||||
Assert.NotNull(costs);
|
||||
Assert.Equal(9, costs.Length);
|
||||
}
|
||||
|
||||
// ── UiText.RightAligned — unit-level draw mode flag ──────────────────────
|
||||
[Fact]
|
||||
public void SampleCharacter_AttributeRaiseCosts_FocusAt110()
|
||||
=> Assert.Equal(110L, SampleData.SampleCharacter().AttributeRaiseCosts[4]);
|
||||
|
||||
[Fact]
|
||||
public void SampleCharacter_AttributeRaiseCosts_StrengthAt0()
|
||||
=> Assert.Equal(0L, SampleData.SampleCharacter().AttributeRaiseCosts[0]);
|
||||
|
||||
// ── UiText flag sanity ────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void UiText_RightAligned_DefaultFalse()
|
||||
|
|
@ -300,13 +644,20 @@ public class CharacterStatControllerTests
|
|||
Assert.True(t.RightAligned);
|
||||
}
|
||||
|
||||
// ── Robustness: a partial layout (missing ids) must not throw ────────────
|
||||
// ── Robustness ────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Bind_MissingElements_DoesNotThrow()
|
||||
=> CharacterStatController.Bind(Fake(), SampleData.SampleCharacter);
|
||||
|
||||
// ── Helper ─────────────────────────────────────────────────────────────
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Manufacture a minimal UiButton with a fake ElementInfo (no dat sprites).</summary>
|
||||
private static UiButton MakeButton()
|
||||
{
|
||||
var info = new ElementInfo { Id = 0, Type = 1 };
|
||||
return new UiButton(info, static _ => (0u, 0, 0));
|
||||
}
|
||||
|
||||
private static ImportedLayout Fake(params (uint id, UiElement e)[] items)
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue