diff --git a/src/AcDream.App/UI/Layout/CharacterStatController.cs b/src/AcDream.App/UI/Layout/CharacterStatController.cs index 4fb6c0c4..ad3e0538 100644 --- a/src/AcDream.App/UI/Layout/CharacterStatController.cs +++ b/src/AcDream.App/UI/Layout/CharacterStatController.cs @@ -138,6 +138,8 @@ public static class CharacterStatController /// Row highlight color — semi-translucent gold, matches retail /// UIStateId.Highlight (0x06) sprite 0x06001397 visual intent. private static readonly Vector4 HighlightBg = new(1f, 0.75f, 0.2f, 0.25f); + private static readonly Vector4 SkillRowBackground = new(0f, 0f, 0f, 1f); + private static readonly Vector4 BuffedSkillGreen = new(0.55f, 1f, 0.55f, 1f); // ── Row layout constants ───────────────────────────────────────────────── // RowHeight 22px + IconSize 16px: retail spec (2026-06-26) says icons ~icon-height @@ -155,8 +157,8 @@ public static class CharacterStatController private const uint SkillHeaderSpecializedSprite = 0x06000F90u; private const uint SkillHeaderTrainedSprite = 0x06000F86u; - private const uint SkillHeaderUntrainedSprite = 0x06000F89u; - private const uint SkillRowBgSprite = 0x06000F98u; + private const uint SkillHeaderUntrainedSprite = 0x06000F98u; + private const uint SkillHeaderUnusableSprite = 0x06000F89u; private const uint RowHighlightSprite = 0x06001397u; private enum CharacterStatTab @@ -583,9 +585,9 @@ public static class CharacterStatController OrderedSkills(data(), CharacterSkillAdvancementClass.Specialized, usableUntrained: null)); AddBucket("Trained Skills", SkillHeaderTrainedSprite, OrderedSkills(data(), CharacterSkillAdvancementClass.Trained, usableUntrained: null)); - AddBucket("Useable Untrained Skills", SkillHeaderUntrainedSprite, + AddBucket("Untrained Skills", SkillHeaderUntrainedSprite, OrderedSkills(data(), CharacterSkillAdvancementClass.Untrained, usableUntrained: true)); - AddBucket("Unuseable Untrained Skills", SkillHeaderUntrainedSprite, + AddBucket("Unusable Skills", SkillHeaderUnusableSprite, OrderedSkills(data(), CharacterSkillAdvancementClass.Untrained, usableUntrained: false)); skillRows = bindings; @@ -606,7 +608,8 @@ public static class CharacterStatController nameText: skill.Name, valueProvider: () => skill.CurrentLevel.ToString(), valueColorProvider: () => SkillValueColor(skill), - backgroundSprite: SkillRowBgSprite, + nameColor: Vector4.One, + backgroundColor: SkillRowBackground, useSelectionBars: false); row.OnClick = () => HandleSkillRowClick(rowIndex, sel, bindings, spriteResolve, data, allRaise1, allRaise10); bindings.Add(new SkillRowBinding(row, skill)); @@ -656,7 +659,7 @@ public static class CharacterStatController Anchors = AnchorEdges.Left | AnchorEdges.Top, }; string captured = title; - label.LinesProvider = () => new[] { new UiText.Line(captured, Gold) }; + label.LinesProvider = () => new[] { new UiText.Line(captured, Vector4.One) }; header.AddChild(label); list.AddChild(header); return header; @@ -700,9 +703,9 @@ public static class CharacterStatController } private static Vector4 SkillValueColor(CharacterSkill skill) - => skill.CurrentLevel > skill.BaseLevel ? new Vector4(0.55f, 1f, 0.55f, 1f) + => skill.CurrentLevel > skill.BaseLevel ? BuffedSkillGreen : skill.CurrentLevel < skill.BaseLevel ? new Vector4(1f, 0.45f, 0.45f, 1f) - : Body; + : Vector4.One; /// /// Handles a row click: toggle (same row → deselect), else select new row. @@ -781,7 +784,7 @@ public static class CharacterStatController { if (spriteResolve is not null) { - row.BackgroundColor = Vector4.Zero; + row.BackgroundColor = SkillRowBackground; row.BackgroundSprite = RowHighlightSprite; row.SpriteResolve = id => { var (h, w, ht) = spriteResolve(id); return (h, w, ht); }; row.UseSelectionBars = true; @@ -796,11 +799,9 @@ public static class CharacterStatController } else { - row.BackgroundColor = Vector4.Zero; - row.BackgroundSprite = spriteResolve is not null ? SkillRowBgSprite : 0u; - row.SpriteResolve = spriteResolve is not null - ? id => { var (h, w, ht) = spriteResolve(id); return (h, w, ht); } - : null; + row.BackgroundColor = SkillRowBackground; + row.BackgroundSprite = 0u; + row.SpriteResolve = null; row.UseSelectionBars = false; } } @@ -931,6 +932,8 @@ public static class CharacterStatController string nameText, Func valueProvider, Func? valueColorProvider = null, + Vector4? nameColor = null, + Vector4? backgroundColor = null, uint backgroundSprite = 0u, bool useSelectionBars = true) { @@ -940,7 +943,7 @@ public static class CharacterStatController Top = top, Width = width, Height = height, - BackgroundColor = Vector4.Zero, // transparent until selected + BackgroundColor = backgroundColor ?? Vector4.Zero, BackgroundSprite = spriteResolve is not null ? backgroundSprite : 0u, SpriteResolve = spriteResolve is not null && backgroundSprite != 0u ? id => { var (h, w, ht) = spriteResolve(id); return (h, w, ht); } @@ -973,6 +976,7 @@ public static class CharacterStatController float nameY = 0f; string capturedName = nameText; + Vector4 capturedNameColor = nameColor ?? Body; var nameEl = new UiText { Left = nameX, @@ -986,7 +990,7 @@ public static class CharacterStatController Padding = 1f, Anchors = AnchorEdges.Left | AnchorEdges.Top, }; - nameEl.LinesProvider = () => new[] { new UiText.Line(capturedName, Body) }; + nameEl.LinesProvider = () => new[] { new UiText.Line(capturedName, capturedNameColor) }; float valueW = width - nameX - nameW - RowPadX; float valueX = nameX + nameW; diff --git a/src/AcDream.App/UI/UiPanel.cs b/src/AcDream.App/UI/UiPanel.cs index de31ecda..b16a5466 100644 --- a/src/AcDream.App/UI/UiPanel.cs +++ b/src/AcDream.App/UI/UiPanel.cs @@ -175,6 +175,9 @@ public class UiClickablePanel : UiPanel { if (UseSelectionBars && BackgroundSprite != 0 && SpriteResolve is { } sr) { + if (BackgroundColor.W > 0f) + ctx.DrawRect(0, 0, Width, Height, BackgroundColor); + // Draw the selection highlight as a thin bar at the TOP and BOTTOM of the row. // The sprite (0x06001397) is 300×32 px — we draw it as horizontal strips at // native height (SelectionBarHeight), stretched to full panel width (UV tile diff --git a/tests/AcDream.App.Tests/UI/Layout/CharacterStatControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/CharacterStatControllerTests.cs index a9dac56d..f58c7c49 100644 --- a/tests/AcDream.App.Tests/UI/Layout/CharacterStatControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/CharacterStatControllerTests.cs @@ -1,6 +1,7 @@ using AcDream.App.Studio; using AcDream.App.UI; using AcDream.App.UI.Layout; +using System.Numerics; namespace AcDream.App.Tests.UI.Layout; @@ -634,20 +635,32 @@ public class CharacterStatControllerTests ClickTab(layout, left: 92f); - var headers = list.Children + var headerPanels = list.Children .Where(c => c is UiPanel and not UiClickablePanel) + .Cast() + .ToList(); + var headers = headerPanels .Select(c => c.Children.OfType().First().LinesProvider()[0].Text) .ToList(); Assert.Equal(new[] { "Specialized Skills", "Trained Skills", - "Useable Untrained Skills", - "Unuseable Untrained Skills", + "Untrained Skills", + "Unusable Skills", }, headers); + Assert.Equal(new[] { 0x06000F90u, 0x06000F86u, 0x06000F98u, 0x06000F89u }, + headerPanels.Select(h => h.BackgroundSprite).ToArray()); + Assert.All(headerPanels, h => + Assert.Equal(Vector4.One, h.Children.OfType().First().LinesProvider()[0].Color)); var rows = list.Children.OfType().ToList(); Assert.Equal(12, rows.Count); + Assert.All(rows, row => + { + Assert.Equal(new Vector4(0f, 0f, 0f, 1f), row.BackgroundColor); + Assert.Equal(0u, row.BackgroundSprite); + }); var rowNames = rows .Select(r => r.Children.OfType().ToList()[1].LinesProvider()[0].Text) .ToList(); @@ -658,6 +671,14 @@ public class CharacterStatControllerTests "Healing", "Jump", "Loyalty", "Run", "Alchemy", "Cooking", "Fletching", }, rowNames); + + var meleeTexts = rows[0].Children.OfType().ToList(); + Assert.Equal(Vector4.One, meleeTexts[1].LinesProvider()[0].Color); + Assert.Equal(new Vector4(0.55f, 1f, 0.55f, 1f), meleeTexts[2].LinesProvider()[0].Color); + + var healingTexts = rows[5].Children.OfType().ToList(); + Assert.Equal(Vector4.One, healingTexts[1].LinesProvider()[0].Color); + Assert.Equal(Vector4.One, healingTexts[2].LinesProvider()[0].Color); } [Fact]