fix(studio): polish character skill rows

This commit is contained in:
Erik 2026-06-26 17:25:09 +02:00
parent 5f92493742
commit ffaf593c76
3 changed files with 47 additions and 19 deletions

View file

@ -138,6 +138,8 @@ public static class CharacterStatController
/// <summary>Row highlight color — semi-translucent gold, matches retail
/// UIStateId.Highlight (0x06) sprite 0x06001397 visual intent.</summary>
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;
/// <summary>
/// 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<string> valueProvider,
Func<Vector4>? 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;

View file

@ -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

View file

@ -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<UiPanel>()
.ToList();
var headers = headerPanels
.Select(c => c.Children.OfType<UiText>().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<UiText>().First().LinesProvider()[0].Color));
var rows = list.Children.OfType<UiClickablePanel>().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<UiText>().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<UiText>().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<UiText>().ToList();
Assert.Equal(Vector4.One, healingTexts[1].LinesProvider()[0].Color);
Assert.Equal(Vector4.One, healingTexts[2].LinesProvider()[0].Color);
}
[Fact]