feat(D.2b): vital bars use retail dat sprites (back track + fill-cropped front)

UiMeter gains SpriteResolve/BackSpriteId/FrontSpriteId; when both are
set, OnDraw draws the empty-track sprite full-width then the colored-fill
sprite UV-cropped to the live fill fraction (left-to-right drain). Falls
back to solid rects when sprite ids are absent, keeping existing behavior
and tests intact.

MarkupDocument.Build() parses `back`/`front` hex attrs on <meter> and
passes `resolve` into every UiMeter.  vitals.xml wires the authoritative
LayoutDesc 0x21000014 sprites (Health 0x06005F3C/3D, Stamina 3E/3F,
Mana 40/41).  The bar prove-out block in GameWindow.cs was already gone.

If the sprites decode as 1x1 magenta at runtime they are paletted
(INDEX16/P8) — the solid-color fallback will display instead and can be
investigated separately.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-14 19:45:54 +02:00
parent 56ee5eff60
commit 84630517e3
4 changed files with 68 additions and 16 deletions

View file

@ -24,6 +24,14 @@ public sealed class UiMeter : UiElement
public Vector4 BgColor { get; set; } = new(0f, 0f, 0f, 0.5f);
public Vector4 LabelColor { get; set; } = new(1f, 1f, 1f, 1f);
/// <summary>Resolver from a RenderSurface DataId to (GL handle, w, h). When set
/// with Back/Front sprite ids, the bar draws the retail sprites instead of solid color.</summary>
public Func<uint, (uint tex, int w, int h)>? SpriteResolve { get; set; }
/// <summary>Empty-track sprite (drawn full width). 0 = none.</summary>
public uint BackSpriteId { get; set; }
/// <summary>Colored-fill sprite (drawn cropped to the fill fraction). 0 = none.</summary>
public uint FrontSpriteId { get; set; }
public UiMeter() { ClickThrough = true; }
/// <summary>Clamp <paramref name="pct"/> to [0,1] and return the fill rect
@ -38,13 +46,32 @@ public sealed class UiMeter : UiElement
protected override void OnDraw(UiRenderContext ctx)
{
ctx.DrawRect(0, 0, Width, Height, BgColor);
float? pct = Fill();
if (pct is float p)
float p = pct is float pf ? (pf < 0f ? 0f : pf > 1f ? 1f : pf) : 0f;
if (SpriteResolve is { } resolve && (BackSpriteId != 0 || FrontSpriteId != 0))
{
var (fx, fy, fw, fh) = ComputeFillRect(p, Width, Height);
if (fw > 0f) ctx.DrawRect(fx, fy, fw, fh, BarColor);
// Retail bar: empty track full width, colored fill cropped to p (left→right).
if (BackSpriteId != 0)
{
var (bt, _, _) = resolve(BackSpriteId);
if (bt != 0) ctx.DrawSprite(bt, 0, 0, Width, Height, 0, 0, 1, 1, Vector4.One);
}
if (FrontSpriteId != 0 && pct is not null && p > 0f)
{
var (ft, _, _) = resolve(FrontSpriteId);
if (ft != 0) ctx.DrawSprite(ft, 0, 0, Width * p, Height, 0, 0, p, 1, Vector4.One);
}
}
else
{
// Placeholder solid-color fallback.
ctx.DrawRect(0, 0, Width, Height, BgColor);
if (pct is not null && p > 0f)
{
var (fx, fy, fw, fh) = ComputeFillRect(p, Width, Height);
if (fw > 0f) ctx.DrawRect(fx, fy, fw, fh, BarColor);
}
}
string? label = Label();