From 84630517e3027356e14b8d77847054e880befd47 Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 14 Jun 2026 19:45:54 +0200 Subject: [PATCH] feat(D.2b): vital bars use retail dat sprites (back track + fill-cropped front) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 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) --- src/AcDream.App/UI/MarkupDocument.cs | 28 ++++++++++---- src/AcDream.App/UI/UiMeter.cs | 37 ++++++++++++++++--- src/AcDream.App/UI/assets/vitals.xml | 6 +-- .../UI/MarkupDocumentTests.cs | 13 +++++++ 4 files changed, 68 insertions(+), 16 deletions(-) diff --git a/src/AcDream.App/UI/MarkupDocument.cs b/src/AcDream.App/UI/MarkupDocument.cs index 3be8a555..5c7baaa3 100644 --- a/src/AcDream.App/UI/MarkupDocument.cs +++ b/src/AcDream.App/UI/MarkupDocument.cs @@ -58,14 +58,17 @@ public static class MarkupDocument var max = BindUint((string?)el.Attribute("max"), binding); panel.AddChild(new UiMeter { - Left = F(el, "x"), - Top = F(el, "y"), - Width = F(el, "w"), - Height = F(el, "h"), - BarColor = Color((string?)el.Attribute("color")), - Fill = BindFloat((string?)el.Attribute("fill"), binding), - Label = () => (cur(), max()) is (uint c, uint m) ? $"{c}/{m}" : null, - Anchors = Anchor((string?)el.Attribute("anchor")), + Left = F(el, "x"), + Top = F(el, "y"), + Width = F(el, "w"), + Height = F(el, "h"), + BarColor = Color((string?)el.Attribute("color")), + Fill = BindFloat((string?)el.Attribute("fill"), binding), + Label = () => (cur(), max()) is (uint c, uint m) ? $"{c}/{m}" : null, + Anchors = Anchor((string?)el.Attribute("anchor")), + SpriteResolve = resolve, + BackSpriteId = Hex((string?)el.Attribute("back")), + FrontSpriteId = Hex((string?)el.Attribute("front")), }); break; // future element kinds (label, button, image) added here @@ -125,6 +128,15 @@ public static class MarkupDocument return binding.GetType().GetProperty(expr[1..^1]); } + private static uint Hex(string? s) + { + if (string.IsNullOrWhiteSpace(s)) return 0; + var t = s.Trim(); + if (t.StartsWith("0x", System.StringComparison.OrdinalIgnoreCase)) t = t[2..]; + return uint.TryParse(t, System.Globalization.NumberStyles.HexNumber, + System.Globalization.CultureInfo.InvariantCulture, out var v) ? v : 0u; + } + private static AnchorEdges Anchor(string? csv) { if (string.IsNullOrWhiteSpace(csv)) return AnchorEdges.Left | AnchorEdges.Top; diff --git a/src/AcDream.App/UI/UiMeter.cs b/src/AcDream.App/UI/UiMeter.cs index ef2883c2..48911c14 100644 --- a/src/AcDream.App/UI/UiMeter.cs +++ b/src/AcDream.App/UI/UiMeter.cs @@ -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); + /// 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. + public Func? SpriteResolve { get; set; } + /// Empty-track sprite (drawn full width). 0 = none. + public uint BackSpriteId { get; set; } + /// Colored-fill sprite (drawn cropped to the fill fraction). 0 = none. + public uint FrontSpriteId { get; set; } + public UiMeter() { ClickThrough = true; } /// Clamp 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(); diff --git a/src/AcDream.App/UI/assets/vitals.xml b/src/AcDream.App/UI/assets/vitals.xml index 08e065d6..2f7292e5 100644 --- a/src/AcDream.App/UI/assets/vitals.xml +++ b/src/AcDream.App/UI/assets/vitals.xml @@ -1,5 +1,5 @@ - - - + + + diff --git a/tests/AcDream.App.Tests/UI/MarkupDocumentTests.cs b/tests/AcDream.App.Tests/UI/MarkupDocumentTests.cs index 5e76ab95..ed717bbd 100644 --- a/tests/AcDream.App.Tests/UI/MarkupDocumentTests.cs +++ b/tests/AcDream.App.Tests/UI/MarkupDocumentTests.cs @@ -56,4 +56,17 @@ public class MarkupDocumentTests Assert.True(panel.ResizeX); Assert.False(panel.ResizeY); } + + [Fact] + public void Build_ParsesBackFrontSpriteIds() + { + const string xml = "" + + "" + + ""; + var panel = MarkupDocument.Build(xml, new FakeBinding(), _ => ((uint)7, 32, 32)); + var meter = Assert.IsType(panel.Children[1]); + Assert.Equal(0x06005F3Cu, meter.BackSpriteId); + Assert.Equal(0x06005F3Du, meter.FrontSpriteId); + Assert.NotNull(meter.SpriteResolve); + } }