feat(ui): D.2b item interaction + retail cursors + live character sheet
Lands the codex-worktree D.2b stream plus the extraction the 2026-07-02 UI architecture review mandated before commit: - ItemInteractionController: single owner of double-click use/equip/ container-open, targeted-use mode (health kits), drag-out drop; toolbar shortcut drags don't drop the real item. ItemEquipRules for multi-slot (coat) coverage via equip masks. - Cursor phase: CursorFeedbackController (semantic priority chain: drag > resize > window-move > target-mode > text) + RetailCursorCatalog (enums 0x27/0x28/0x29, hotspot 14,14; ClientUISystem::UpdateCursorState 0x00564630) resolved through the portal EnumIDMap chain by RetailCursorResolver; RetailCursorManager applies dat cursor art to the OS cursor. Register row AP-72 covers the OS standard-cursor fallback. - Character window goes live: CharacterSheetProvider owns sheet assembly, XP-curve/raise-cost math and the raise flow — extracted out of GameWindow per Code Structure Rule 1 instead of committing the ~430-line feature body there. Optimistic XP/credit debits go through eventful store APIs (new ClientObjectTable.UpdateInt64Property + LocalPlayerState.DebitIntProperty/DebitInt64Property) instead of raw property-dictionary writes; register row AP-73 covers the still-missing raise ledger (#163). - RetailWindowFrame: the shared nine-slice window mount recipe; the character window uses it, remaining windows migrate via #164. - Status-bar buttons toggle inventory/character windows; retail row-major backpack ordering; WorldSession.SendUseWithTarget + raise/train sends. GameWindow shrinks 14,214 -> 13,877 lines despite the new features; the sheet/raise logic is unit-tested in CharacterSheetProviderTests instead of trapped in the god object. Build green; full suite 3,286 tests pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
e3fc7ac5ba
commit
b7dc91a053
74 changed files with 6669 additions and 238 deletions
|
|
@ -50,10 +50,114 @@ public class CharacterStatControllerTests
|
|||
|
||||
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
|
||||
|
||||
Assert.Equal("Aluvian Heritage", heritage.LinesProvider()[0].Text);
|
||||
Assert.Equal("Female Aluvian Adventurer", heritage.LinesProvider()[0].Text);
|
||||
Assert.Equal("Non-Player Killer", pk.LinesProvider()[0].Text);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bind_WindowChromeButton_InvokesCloseCallback()
|
||||
{
|
||||
var close = MakeButton(WindowChromeController.CharacterCloseButtonId);
|
||||
var layout = Fake((WindowChromeController.CharacterCloseButtonId, close));
|
||||
int closes = 0;
|
||||
|
||||
CharacterStatController.Bind(layout, SampleData.SampleCharacter, onClose: () => closes++);
|
||||
close.OnEvent(new UiEvent(0u, close, UiEventType.Click));
|
||||
|
||||
Assert.Equal(1, closes);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CharacterIdentityText_StatHeaderLine_ComposesRetailGenderHeritageTitle()
|
||||
{
|
||||
var sheet = new CharacterSheet
|
||||
{
|
||||
Gender = "Female",
|
||||
Heritage = "Aluvian",
|
||||
Title = "the Adventurer",
|
||||
};
|
||||
|
||||
Assert.Equal("Female Aluvian Adventurer", CharacterIdentityText.StatHeaderLine(sheet));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(1, "Male")]
|
||||
[InlineData(2, "Female")]
|
||||
[InlineData(0, null)]
|
||||
public void CharacterIdentityText_GenderDisplayName_UsesRetailEnum(int value, string? expected)
|
||||
=> Assert.Equal(expected, CharacterIdentityText.GenderDisplayName(value));
|
||||
|
||||
[Theory]
|
||||
[InlineData(1, "Aluvian")]
|
||||
[InlineData(2, "Gharu'ndim")]
|
||||
[InlineData(5, "Umbraen")]
|
||||
[InlineData(13, "Olthoi")]
|
||||
[InlineData(99, null)]
|
||||
public void CharacterIdentityText_HeritageGroupDisplayName_UsesRetailEnum(int value, string? expected)
|
||||
=> Assert.Equal(expected, CharacterIdentityText.HeritageGroupDisplayName(value));
|
||||
|
||||
[Fact]
|
||||
public void Bind_HeaderElements_UseVisibleAttributesPageWhenIdsAreDuplicated()
|
||||
{
|
||||
var root = new UiPanel { Width = 300, Height = 600 };
|
||||
var attrPage = MakeDatElement(CharacterStatController.AttributesPageId, top: 25, width: 300, height: 575);
|
||||
var hiddenPage = MakeDatElement(CharacterStatController.SkillsPageId, top: 25, width: 300, height: 575);
|
||||
|
||||
var visibleName = new UiText { ElementId = CharacterStatController.NameId };
|
||||
var hiddenName = new UiText { ElementId = CharacterStatController.NameId };
|
||||
var visibleHeritage = new UiText { ElementId = CharacterStatController.HeritageId };
|
||||
var hiddenHeritage = new UiText { ElementId = CharacterStatController.HeritageId };
|
||||
var visibleLevel = new UiText { ElementId = CharacterStatController.LevelId };
|
||||
var hiddenLevel = new UiText { ElementId = CharacterStatController.LevelId };
|
||||
var visibleTotalXp = new UiText { ElementId = CharacterStatController.TotalXpId };
|
||||
var hiddenTotalXp = new UiText { ElementId = CharacterStatController.TotalXpId };
|
||||
var visibleTotalXpLabel = new UiText { ElementId = CharacterStatController.TotalXpLabelId };
|
||||
var hiddenTotalXpLabel = new UiText { ElementId = CharacterStatController.TotalXpLabelId };
|
||||
var visibleMeter = new UiMeter { ElementId = CharacterStatController.XpMeterId };
|
||||
var hiddenMeter = new UiMeter { ElementId = CharacterStatController.XpMeterId };
|
||||
var visibleXpNext = new UiText { ElementId = CharacterStatController.XpNextValueId };
|
||||
var hiddenXpNext = new UiText { ElementId = CharacterStatController.XpNextValueId };
|
||||
visibleMeter.AddChild(visibleXpNext);
|
||||
hiddenMeter.AddChild(hiddenXpNext);
|
||||
|
||||
attrPage.AddChild(visibleName);
|
||||
attrPage.AddChild(visibleHeritage);
|
||||
attrPage.AddChild(visibleLevel);
|
||||
attrPage.AddChild(visibleTotalXpLabel);
|
||||
attrPage.AddChild(visibleTotalXp);
|
||||
attrPage.AddChild(visibleMeter);
|
||||
hiddenPage.AddChild(hiddenName);
|
||||
hiddenPage.AddChild(hiddenHeritage);
|
||||
hiddenPage.AddChild(hiddenLevel);
|
||||
hiddenPage.AddChild(hiddenTotalXpLabel);
|
||||
hiddenPage.AddChild(hiddenTotalXp);
|
||||
hiddenPage.AddChild(hiddenMeter);
|
||||
root.AddChild(attrPage);
|
||||
root.AddChild(hiddenPage);
|
||||
|
||||
var layout = new ImportedLayout(root, new Dictionary<uint, UiElement>
|
||||
{
|
||||
[CharacterStatController.NameId] = hiddenName,
|
||||
[CharacterStatController.HeritageId] = hiddenHeritage,
|
||||
[CharacterStatController.LevelId] = hiddenLevel,
|
||||
[CharacterStatController.TotalXpLabelId] = hiddenTotalXpLabel,
|
||||
[CharacterStatController.TotalXpId] = hiddenTotalXp,
|
||||
[CharacterStatController.XpMeterId] = hiddenMeter,
|
||||
[CharacterStatController.XpNextValueId] = hiddenXpNext,
|
||||
});
|
||||
|
||||
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
|
||||
|
||||
Assert.Equal("Studio Player", visibleName.LinesProvider()[0].Text);
|
||||
Assert.Equal("Female Aluvian Adventurer", visibleHeritage.LinesProvider()[0].Text);
|
||||
Assert.Equal("126", visibleLevel.LinesProvider()[0].Text);
|
||||
Assert.Equal("Total Experience (XP):", visibleTotalXpLabel.LinesProvider()[0].Text);
|
||||
Assert.Equal((1_250_000_000L).ToString("N0"), visibleTotalXp.LinesProvider()[0].Text);
|
||||
Assert.Equal((42_000_000L).ToString("N0"), visibleXpNext.LinesProvider()[0].Text);
|
||||
Assert.Empty(hiddenName.LinesProvider());
|
||||
Assert.Empty(hiddenXpNext.LinesProvider());
|
||||
}
|
||||
|
||||
// ── XP meter fill ────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
|
|
@ -514,6 +618,31 @@ public class CharacterStatControllerTests
|
|||
Assert.Equal("Normal", btn10.ActiveState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RaiseButtons_OnlyOneAffordable_SplitsOneAndTenStates()
|
||||
{
|
||||
var btn1 = MakeButton();
|
||||
var btn10 = MakeButton();
|
||||
var list = new UiPanel();
|
||||
var layout = Fake(
|
||||
(CharacterStatController.RaiseOneId, btn1),
|
||||
(CharacterStatController.RaiseTenId, btn10),
|
||||
(CharacterStatController.ListBoxId, list));
|
||||
var sheet = new CharacterSheet
|
||||
{
|
||||
UnassignedXp = 500L,
|
||||
AttributeRaiseCosts = new long[] { 0L, 0L, 0L, 0L, 110L, 0L, 0L, 0L, 0L },
|
||||
AttributeRaise10Costs = new long[] { 0L, 0L, 0L, 0L, 1_100L, 0L, 0L, 0L, 0L },
|
||||
};
|
||||
|
||||
CharacterStatController.Bind(layout, () => sheet);
|
||||
|
||||
list.Children.OfType<UiClickablePanel>().ToList()[4].OnClick!();
|
||||
|
||||
Assert.Equal("Normal", btn1.ActiveState);
|
||||
Assert.Equal("Ghosted", btn10.ActiveState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RaiseButtons_MaxedRow_ShowsGhostedState()
|
||||
{
|
||||
|
|
@ -535,6 +664,95 @@ public class CharacterStatControllerTests
|
|||
Assert.Equal("Ghosted", btn10.ActiveState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RaiseButtons_ClickAffordableAttribute_EmitsRaiseRequest()
|
||||
{
|
||||
var btn1 = MakeButton();
|
||||
var list = new UiPanel();
|
||||
var layout = Fake(
|
||||
(CharacterStatController.RaiseOneId, btn1),
|
||||
(CharacterStatController.ListBoxId, list));
|
||||
var requests = new List<CharacterStatController.RaiseRequest>();
|
||||
|
||||
CharacterStatController.Bind(layout, SampleData.SampleCharacter, onRaiseRequest: requests.Add);
|
||||
|
||||
list.Children.OfType<UiClickablePanel>().ToList()[4].OnClick!();
|
||||
btn1.OnClick!();
|
||||
|
||||
var request = Assert.Single(requests);
|
||||
Assert.Equal(CharacterStatController.RaiseTargetKind.Attribute, request.Kind);
|
||||
Assert.Equal(5u, request.StatId);
|
||||
Assert.Equal(110L, request.Cost);
|
||||
Assert.Equal(1, request.Amount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RaiseButtons_ClickAffordableAttribute_KeepsNormalStateUntilCostsRefresh()
|
||||
{
|
||||
var btn1 = MakeButton();
|
||||
var list = new UiPanel();
|
||||
var layout = Fake(
|
||||
(CharacterStatController.RaiseOneId, btn1),
|
||||
(CharacterStatController.ListBoxId, list));
|
||||
var requests = new List<CharacterStatController.RaiseRequest>();
|
||||
|
||||
CharacterStatController.Bind(layout, SampleData.SampleCharacter, onRaiseRequest: requests.Add);
|
||||
|
||||
list.Children.OfType<UiClickablePanel>().ToList()[4].OnClick!();
|
||||
Assert.Equal("Normal", btn1.ActiveState);
|
||||
|
||||
btn1.OnClick!();
|
||||
|
||||
Assert.Single(requests);
|
||||
Assert.Equal("Normal", btn1.ActiveState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RaiseButtons_ClickAffordableVital_EmitsMaxVitalId()
|
||||
{
|
||||
var btn1 = MakeButton();
|
||||
var list = new UiPanel();
|
||||
var layout = Fake(
|
||||
(CharacterStatController.RaiseOneId, btn1),
|
||||
(CharacterStatController.ListBoxId, list));
|
||||
var requests = new List<CharacterStatController.RaiseRequest>();
|
||||
|
||||
CharacterStatController.Bind(layout, SampleData.SampleCharacter, onRaiseRequest: requests.Add);
|
||||
|
||||
list.Children.OfType<UiClickablePanel>().ToList()[6].OnClick!();
|
||||
btn1.OnClick!();
|
||||
|
||||
var request = Assert.Single(requests);
|
||||
Assert.Equal(CharacterStatController.RaiseTargetKind.Vital, request.Kind);
|
||||
Assert.Equal(1u, request.StatId);
|
||||
Assert.Equal(90L, request.Cost);
|
||||
Assert.Equal(1, request.Amount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RaiseButtons_ClickUnaffordableTen_DoesNotEmitRequest()
|
||||
{
|
||||
var btn10 = MakeButton();
|
||||
var list = new UiPanel();
|
||||
var layout = Fake(
|
||||
(CharacterStatController.RaiseTenId, btn10),
|
||||
(CharacterStatController.ListBoxId, list));
|
||||
var requests = new List<CharacterStatController.RaiseRequest>();
|
||||
var sheet = new CharacterSheet
|
||||
{
|
||||
UnassignedXp = 500L,
|
||||
AttributeRaiseCosts = new long[] { 0L, 0L, 0L, 0L, 110L, 0L, 0L, 0L, 0L },
|
||||
AttributeRaise10Costs = new long[] { 0L, 0L, 0L, 0L, 1_100L, 0L, 0L, 0L, 0L },
|
||||
};
|
||||
|
||||
CharacterStatController.Bind(layout, () => sheet, onRaiseRequest: requests.Add);
|
||||
|
||||
list.Children.OfType<UiClickablePanel>().ToList()[4].OnClick!();
|
||||
btn10.OnClick!();
|
||||
|
||||
Assert.Empty(requests);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RaiseButtons_Deselect_HidesButtons()
|
||||
{
|
||||
|
|
@ -635,10 +853,7 @@ public class CharacterStatControllerTests
|
|||
|
||||
ClickTab(layout, left: 92f);
|
||||
|
||||
var headerPanels = list.Children
|
||||
.Where(c => c is UiPanel and not UiClickablePanel)
|
||||
.Cast<UiPanel>()
|
||||
.ToList();
|
||||
var headerPanels = SkillHeaders(list);
|
||||
var headers = headerPanels
|
||||
.Select(c => c.Children.OfType<UiText>().First().LinesProvider()[0].Text)
|
||||
.ToList();
|
||||
|
|
@ -654,7 +869,7 @@ public class CharacterStatControllerTests
|
|||
Assert.All(headerPanels, h =>
|
||||
Assert.Equal(Vector4.One, h.Children.OfType<UiText>().First().LinesProvider()[0].Color));
|
||||
|
||||
var rows = list.Children.OfType<UiClickablePanel>().ToList();
|
||||
var rows = SkillRows(list);
|
||||
Assert.Equal(12, rows.Count);
|
||||
Assert.All(rows, row =>
|
||||
{
|
||||
|
|
@ -692,7 +907,7 @@ public class CharacterStatControllerTests
|
|||
spriteResolve: id => (id, 16, 16));
|
||||
|
||||
ClickTab(layout, left: 92f);
|
||||
Assert.Equal(12, list.Children.OfType<UiClickablePanel>().Count());
|
||||
Assert.Equal(12, SkillRows(list).Count);
|
||||
|
||||
ClickTab(layout, left: 0f);
|
||||
var rows = list.Children.OfType<UiClickablePanel>().ToList();
|
||||
|
|
@ -704,8 +919,8 @@ public class CharacterStatControllerTests
|
|||
public void SkillsTab_MouseClick_RebuildsVisibleListWhenListIdIsDuplicated()
|
||||
{
|
||||
var root = new UiPanel { Width = 300, Height = 600 };
|
||||
var attrPage = new UiPanel { Top = 25, Width = 300, Height = 575 };
|
||||
var hiddenPage = new UiPanel { Top = 25, Width = 300, Height = 575 };
|
||||
var attrPage = MakeDatElement(CharacterStatController.AttributesPageId, top: 25, width: 300, height: 575);
|
||||
var hiddenPage = MakeDatElement(CharacterStatController.SkillsPageId, top: 25, width: 300, height: 575);
|
||||
var name = new UiText();
|
||||
var visibleList = MakeDatElement(CharacterStatController.ListBoxId, top: 112, width: 300, height: 398);
|
||||
var hiddenDuplicateList = MakeDatElement(CharacterStatController.ListBoxId, top: 112, width: 300, height: 398);
|
||||
|
|
@ -761,7 +976,7 @@ public class CharacterStatControllerTests
|
|||
spriteResolve: id => (id, 16, 16));
|
||||
|
||||
ClickTab(layout, left: 92f);
|
||||
var rows = list.Children.OfType<UiClickablePanel>().ToList();
|
||||
var rows = SkillRows(list);
|
||||
rows[1].OnClick!();
|
||||
|
||||
Assert.Equal("War Magic: 285", title.LinesProvider()[0].Text);
|
||||
|
|
@ -777,6 +992,31 @@ public class CharacterStatControllerTests
|
|||
Assert.Equal(Vector4.Zero, rows[0].BackgroundColor);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SkillsTab_ClickRaiseTen_EmitsSkillRaiseRequest()
|
||||
{
|
||||
var list = new UiPanel { Width = 300 };
|
||||
var btn10 = MakeButton();
|
||||
var layout = Fake(
|
||||
(CharacterStatController.ListBoxId, list),
|
||||
(CharacterStatController.RaiseTenId, btn10));
|
||||
var requests = new List<CharacterStatController.RaiseRequest>();
|
||||
|
||||
CharacterStatController.Bind(layout, SampleData.SampleCharacter,
|
||||
spriteResolve: id => (id, 16, 16),
|
||||
onRaiseRequest: requests.Add);
|
||||
|
||||
ClickTab(layout, left: 92f);
|
||||
SkillRows(list)[1].OnClick!();
|
||||
btn10.OnClick!();
|
||||
|
||||
var request = Assert.Single(requests);
|
||||
Assert.Equal(CharacterStatController.RaiseTargetKind.Skill, request.Kind);
|
||||
Assert.Equal(34u, request.StatId);
|
||||
Assert.Equal(111_000_000L, request.Cost);
|
||||
Assert.Equal(10, request.Amount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SkillsTab_SelectHealing_ShowsUntrainedSkillFooter()
|
||||
{
|
||||
|
|
@ -800,7 +1040,7 @@ public class CharacterStatControllerTests
|
|||
spriteResolve: id => (id, 16, 16));
|
||||
|
||||
ClickTab(layout, left: 92f);
|
||||
list.Children.OfType<UiClickablePanel>().ToList()[5].OnClick!();
|
||||
SkillRows(list)[5].OnClick!();
|
||||
|
||||
Assert.Equal("Healing", title.LinesProvider()[0].Text);
|
||||
Assert.Equal("Skill Credits To Raise:", l1Label.LinesProvider()[0].Text);
|
||||
|
|
@ -811,6 +1051,133 @@ public class CharacterStatControllerTests
|
|||
Assert.Equal("Normal", btn1.ActiveState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SkillsTab_ClickUntrainedSkill_EmitsTrainSkillRequest()
|
||||
{
|
||||
var list = new UiPanel { Width = 300 };
|
||||
var btn1 = MakeButton();
|
||||
var layout = Fake(
|
||||
(CharacterStatController.ListBoxId, list),
|
||||
(CharacterStatController.RaiseOneId, btn1));
|
||||
var requests = new List<CharacterStatController.RaiseRequest>();
|
||||
|
||||
CharacterStatController.Bind(layout, SampleData.SampleCharacter,
|
||||
spriteResolve: id => (id, 16, 16),
|
||||
onRaiseRequest: requests.Add);
|
||||
|
||||
ClickTab(layout, left: 92f);
|
||||
SkillRows(list)[5].OnClick!();
|
||||
btn1.OnClick!();
|
||||
|
||||
var request = Assert.Single(requests);
|
||||
Assert.Equal(CharacterStatController.RaiseTargetKind.TrainSkill, request.Kind);
|
||||
Assert.Equal(21u, request.StatId);
|
||||
Assert.Equal(6L, request.Cost);
|
||||
Assert.Equal(1, request.Amount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SkillsTab_ClickTrain_RebuildsSelectedSkillAsTrained()
|
||||
{
|
||||
var list = new UiPanel { Width = 300 };
|
||||
var btn1 = MakeButton();
|
||||
var btn10 = MakeButton();
|
||||
CharacterSheet sheet = new()
|
||||
{
|
||||
SkillCredits = 10,
|
||||
UnassignedXp = 1_000,
|
||||
Skills = new[]
|
||||
{
|
||||
new CharacterSkill(100u, "Train Me", 0x06000001u,
|
||||
CharacterSkillAdvancementClass.Untrained,
|
||||
BaseLevel: 5,
|
||||
CurrentLevel: 5,
|
||||
UsableUntrained: true,
|
||||
TrainedCost: 4,
|
||||
SpecializedCost: 0,
|
||||
RaiseCost: 0,
|
||||
Raise10Cost: 0),
|
||||
},
|
||||
};
|
||||
var layout = Fake(
|
||||
(CharacterStatController.ListBoxId, list),
|
||||
(CharacterStatController.RaiseOneId, btn1),
|
||||
(CharacterStatController.RaiseTenId, btn10));
|
||||
var requests = new List<CharacterStatController.RaiseRequest>();
|
||||
|
||||
CharacterStatController.Bind(layout, () => sheet,
|
||||
spriteResolve: id => (id, 16, 16),
|
||||
onRaiseRequest: request =>
|
||||
{
|
||||
requests.Add(request);
|
||||
sheet = new CharacterSheet
|
||||
{
|
||||
SkillCredits = 6,
|
||||
UnassignedXp = 1_000,
|
||||
Skills = new[]
|
||||
{
|
||||
new CharacterSkill(100u, "Train Me", 0x06000001u,
|
||||
CharacterSkillAdvancementClass.Trained,
|
||||
BaseLevel: 5,
|
||||
CurrentLevel: 5,
|
||||
UsableUntrained: true,
|
||||
TrainedCost: 4,
|
||||
SpecializedCost: 0,
|
||||
RaiseCost: 10,
|
||||
Raise10Cost: 100),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
ClickTab(layout, left: 92f);
|
||||
SkillRows(list).Single().OnClick!();
|
||||
Assert.False(btn10.Visible);
|
||||
|
||||
btn1.OnClick!();
|
||||
|
||||
var request = Assert.Single(requests);
|
||||
Assert.Equal(CharacterStatController.RaiseTargetKind.TrainSkill, request.Kind);
|
||||
Assert.Equal("Train Me", SkillRows(list).Single().Children.OfType<UiText>().ToList()[1].LinesProvider()[0].Text);
|
||||
Assert.True(btn1.Visible);
|
||||
Assert.True(btn10.Visible);
|
||||
Assert.Equal("Normal", btn1.ActiveState);
|
||||
Assert.Equal("Normal", btn10.ActiveState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SkillsTab_BindsCharacterScrollbarToScrollableViewport()
|
||||
{
|
||||
var root = new UiPanel { Width = 300, Height = 600 };
|
||||
var page = new UiPanel { Width = 300, Height = 600 };
|
||||
var name = new UiText();
|
||||
var list = MakeDatElement(CharacterStatController.ListBoxId, top: 137, width: 300, height: 80);
|
||||
var scrollbarShell = MakeDatElement(CharacterStatController.ListScrollbarId, top: 137, width: 16, height: 80);
|
||||
scrollbarShell.Left = 281;
|
||||
|
||||
page.AddChild(name);
|
||||
page.AddChild(list);
|
||||
page.AddChild(scrollbarShell);
|
||||
root.AddChild(page);
|
||||
|
||||
var layout = new ImportedLayout(root, new Dictionary<uint, UiElement>
|
||||
{
|
||||
[CharacterStatController.NameId] = name,
|
||||
[CharacterStatController.ListBoxId] = list,
|
||||
[CharacterStatController.ListScrollbarId] = scrollbarShell,
|
||||
});
|
||||
|
||||
CharacterStatController.Bind(layout, SampleData.SampleCharacter,
|
||||
spriteResolve: id => (id, 16, 16));
|
||||
|
||||
ClickTab(layout, left: 92f);
|
||||
|
||||
var bar = page.Children.OfType<UiScrollbar>().Single();
|
||||
Assert.True(bar.Visible);
|
||||
Assert.NotNull(bar.Model);
|
||||
Assert.Contains(list.Children, c => c is UiScrollablePanel);
|
||||
Assert.False(scrollbarShell.Visible);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetRaiseCost_Index4Focus_Returns110()
|
||||
{
|
||||
|
|
@ -818,6 +1185,13 @@ public class CharacterStatControllerTests
|
|||
Assert.Equal(110L, CharacterStatController.GetRaiseCost(sheet, 4));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetRaiseCost_Amount10Index4Focus_Returns1100()
|
||||
{
|
||||
var sheet = SampleData.SampleCharacter();
|
||||
Assert.Equal(1_100L, CharacterStatController.GetRaiseCost(sheet, 4, amount: 10));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetRaiseCost_Index0Strength_Returns0()
|
||||
{
|
||||
|
|
@ -868,10 +1242,22 @@ public class CharacterStatControllerTests
|
|||
Assert.Equal(9, costs.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SampleCharacter_AttributeRaise10Costs_HasNineEntries()
|
||||
{
|
||||
var costs = SampleData.SampleCharacter().AttributeRaise10Costs;
|
||||
Assert.NotNull(costs);
|
||||
Assert.Equal(9, costs.Length);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SampleCharacter_AttributeRaiseCosts_FocusAt110()
|
||||
=> Assert.Equal(110L, SampleData.SampleCharacter().AttributeRaiseCosts[4]);
|
||||
|
||||
[Fact]
|
||||
public void SampleCharacter_AttributeRaise10Costs_FocusAt1100()
|
||||
=> Assert.Equal(1_100L, SampleData.SampleCharacter().AttributeRaise10Costs[4]);
|
||||
|
||||
[Fact]
|
||||
public void SampleCharacter_AttributeRaiseCosts_StrengthAt0()
|
||||
=> Assert.Equal(0L, SampleData.SampleCharacter().AttributeRaiseCosts[0]);
|
||||
|
|
@ -1098,12 +1484,32 @@ public class CharacterStatControllerTests
|
|||
|
||||
private static string FirstRowName(UiElement list)
|
||||
{
|
||||
var row = list.Children.OfType<UiClickablePanel>().FirstOrDefault();
|
||||
var row = Descendants(list).OfType<UiClickablePanel>().FirstOrDefault();
|
||||
if (row is null) return "<no row>";
|
||||
var texts = row.Children.OfType<UiText>().ToList();
|
||||
return texts.Count > 1 ? texts[1].LinesProvider()[0].Text : "<no text>";
|
||||
}
|
||||
|
||||
private static List<UiClickablePanel> SkillRows(UiElement list)
|
||||
=> Descendants(list).OfType<UiClickablePanel>().ToList();
|
||||
|
||||
private static List<UiPanel> SkillHeaders(UiElement list)
|
||||
=> Descendants(list)
|
||||
.Where(c => c is UiPanel and not UiClickablePanel
|
||||
&& c.Children.OfType<UiText>().Any())
|
||||
.Cast<UiPanel>()
|
||||
.ToList();
|
||||
|
||||
private static IEnumerable<UiElement> Descendants(UiElement root)
|
||||
{
|
||||
foreach (var child in root.Children)
|
||||
{
|
||||
yield return child;
|
||||
foreach (var nested in Descendants(child))
|
||||
yield return nested;
|
||||
}
|
||||
}
|
||||
|
||||
private static UiDatElement MakeDatElement(uint id, float top, float width, float height)
|
||||
{
|
||||
var info = new ElementInfo
|
||||
|
|
@ -1123,9 +1529,9 @@ public class CharacterStatControllerTests
|
|||
}
|
||||
|
||||
/// <summary>Manufacture a minimal UiButton with a fake ElementInfo (no dat sprites).</summary>
|
||||
private static UiButton MakeButton()
|
||||
private static UiButton MakeButton(uint id = 0u)
|
||||
{
|
||||
var info = new ElementInfo { Id = 0, Type = 1 };
|
||||
var info = new ElementInfo { Id = id, Type = 1 };
|
||||
return new UiButton(info, static _ => (0u, 0, 0));
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue