TextRenderer batched sprites per-texture and drew each texture's whole buffer at its FIRST-insertion point. The dat-font glyph atlas is one shared texture used by all three vital numbers; it first appeared at the health bar, so all three numbers were emitted right after the health bars — then the stamina + mana bar sprites painted over their own numbers (only health survived). Replaced the per-texture dictionary with submission-ordered segments (consecutive same-texture quads still batch); each meter's number now draws after its own bars. The renderer's own comment had predicted this break once bars became sprites (importer did that). Removed the temporary UiMeter label diagnostic. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
167 lines
8.1 KiB
C#
167 lines
8.1 KiB
C#
using System.Numerics;
|
|
|
|
namespace AcDream.App.UI;
|
|
|
|
/// <summary>
|
|
/// A horizontal vital bar (retail HP/Stamina/Mana style): a background rect, a
|
|
/// partial-width solid fill, and an optional centered "current/max" numeric
|
|
/// overlay. <see cref="Fill"/> returns 0..1 (null = no data → empty bar);
|
|
/// <see cref="Label"/> returns the overlay text (null = no number).
|
|
///
|
|
/// <para>
|
|
/// Solid-color fill + debug font for Spec 1. The retail gradient bar sprite
|
|
/// (glassy center highlight) and the retail dat font are a later polish pass —
|
|
/// retail's vitals are bars exactly like this, just sprited.
|
|
/// </para>
|
|
/// </summary>
|
|
public sealed class UiMeter : UiElement
|
|
{
|
|
|
|
/// <summary>Fill fraction provider; a null result draws an empty bar.</summary>
|
|
public Func<float?> Fill { get; set; } = () => 0f;
|
|
/// <summary>Centered overlay text provider (e.g. "291/291"); null = none.</summary>
|
|
public Func<string?> Label { get; set; } = () => null;
|
|
public Vector4 BarColor { get; set; } = new(1f, 0f, 0f, 1f);
|
|
public Vector4 BgColor { get; set; } = new(0f, 0f, 0f, 0.5f);
|
|
public Vector4 LabelColor { get; set; } = new(1f, 1f, 1f, 1f);
|
|
|
|
/// <summary>Retail dat font (Font 0x40000000) for the "cur/max" overlay. When
|
|
/// set, the label renders through the dat-font two-pass blit (outline + fill);
|
|
/// when null, the debug <see cref="UiRenderContext.DefaultFont"/> bitmap font
|
|
/// is used instead. Set by the host when the retail UI is active.</summary>
|
|
public UiDatFont? DatFont { get; set; }
|
|
|
|
/// <summary>Resolver from a RenderSurface DataId to (GL handle, w, h). When set
|
|
/// with the 9-slice ids below, the bar draws the retail sprites instead of solid color.</summary>
|
|
public Func<uint, (uint tex, int w, int h)>? SpriteResolve { get; set; }
|
|
|
|
// Retail vital bars are a horizontal 3-slice: a fixed-width bevelled left-cap,
|
|
// a TILED gradient middle (the "fill-tile" repeats at native width — it does not
|
|
// stretch), and a fixed-width right-cap. The "back" slice is the empty track
|
|
// (drawn full width); the "front" slice is the coloured fill (drawn full-geometry
|
|
// but CLIPPED to the fill fraction — its own right-cap shows at 100%, the back's
|
|
// shows through when partial). Ids come from the stacked vitals LayoutDesc
|
|
// (0x2100006C) via the dump-vitals-layout CLI; 0 = none.
|
|
/// <summary>Empty-track left-cap RenderSurface id.</summary>
|
|
public uint BackLeft { get; set; }
|
|
/// <summary>Empty-track middle (tiled gradient) RenderSurface id.</summary>
|
|
public uint BackTile { get; set; }
|
|
/// <summary>Empty-track right-cap RenderSurface id.</summary>
|
|
public uint BackRight { get; set; }
|
|
/// <summary>Coloured-fill left-cap RenderSurface id.</summary>
|
|
public uint FrontLeft { get; set; }
|
|
/// <summary>Coloured-fill middle (tiled gradient) RenderSurface id.</summary>
|
|
public uint FrontTile { get; set; }
|
|
/// <summary>Coloured-fill right-cap RenderSurface id.</summary>
|
|
public uint FrontRight { get; set; }
|
|
|
|
public UiMeter() { ClickThrough = true; }
|
|
|
|
/// <summary>Clamp <paramref name="pct"/> to [0,1] and return the fill rect
|
|
/// (local px) for a bar of <paramref name="w"/> x <paramref name="h"/>.</summary>
|
|
public static (float x, float y, float w, float h) ComputeFillRect(
|
|
float pct, float w, float h)
|
|
{
|
|
if (pct < 0f) pct = 0f;
|
|
if (pct > 1f) pct = 1f;
|
|
return (0f, 0f, w * pct, h);
|
|
}
|
|
|
|
protected override void OnDraw(UiRenderContext ctx)
|
|
{
|
|
float? pct = Fill();
|
|
float p = pct is float pf ? (pf < 0f ? 0f : pf > 1f ? 1f : pf) : 0f;
|
|
|
|
if (SpriteResolve is { } resolve && (BackLeft != 0 || BackTile != 0 || FrontTile != 0))
|
|
{
|
|
// Retail meter (UIElement_Meter::DrawChildren): the BACK 3-slice is the
|
|
// empty track, drawn full width; the FRONT 3-slice is the coloured fill,
|
|
// drawn at FULL width too but horizontally CLIPPED to the fill fraction.
|
|
// The front carries its own right-cap (shown at 100%); clipping below 100%
|
|
// removes it and reveals the back track's right-cap — retail's scissor-fill.
|
|
DrawHBar(ctx, resolve, BackLeft, BackTile, BackRight, Width);
|
|
if (pct is not null && p > 0f)
|
|
DrawHBar(ctx, resolve, FrontLeft, FrontTile, FrontRight, Width * p);
|
|
}
|
|
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();
|
|
if (!string.IsNullOrEmpty(label))
|
|
{
|
|
if (DatFont is { } datFont)
|
|
{
|
|
// Retail path: centered cur/max via the dat font's two-pass blit.
|
|
float tw = datFont.MeasureWidth(label);
|
|
float tx = (Width - tw) * 0.5f;
|
|
float ty = (Height - datFont.LineHeight) * 0.5f;
|
|
ctx.DrawStringDat(datFont, label, tx, ty, LabelColor);
|
|
}
|
|
else if (ctx.DefaultFont is { } font)
|
|
{
|
|
// Fallback: debug bitmap font (no dat font available).
|
|
float tw = font.MeasureWidth(label);
|
|
float tx = (Width - tw) * 0.5f;
|
|
float ty = (Height - font.LineHeight) * 0.5f;
|
|
ctx.DrawString(label, tx, ty, LabelColor);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Draws the full-width horizontal 3-slice (native-width left-cap, stretched
|
|
/// middle, native-width right-cap) over this meter's rect, horizontally CLIPPED
|
|
/// so nothing past <paramref name="clipW"/> (local px from the left) is drawn.
|
|
/// The back track passes <c>clipW = Width</c>; the front fill passes
|
|
/// <c>clipW = Width * fraction</c>. Clipping UV-crops each slice proportionally,
|
|
/// so the fill ends cleanly and the back's right-cap shows through when partial.
|
|
/// A 0 id skips that slice.
|
|
/// </summary>
|
|
private void DrawHBar(
|
|
UiRenderContext ctx, Func<uint, (uint tex, int w, int h)> resolve,
|
|
uint leftId, uint midId, uint rightId, float clipW)
|
|
{
|
|
if (clipW <= 0f) return;
|
|
float w = Width, h = Height;
|
|
var (lt, lw, _) = resolve(leftId);
|
|
var (mt, mw, _) = resolve(midId);
|
|
var (rt, rw, _) = resolve(rightId);
|
|
|
|
float capL = lt != 0 ? MathF.Min(lw, w) : 0f;
|
|
float capR = rt != 0 ? MathF.Min(rw, w - capL) : 0f;
|
|
float midW = w - capL - capR;
|
|
|
|
// Each slice's texture repeats every NATIVE-width px (UV-repeat; the UI
|
|
// texture is GL_REPEAT-wrapped — TextureCache.UploadRgba8). Caps span their
|
|
// own native width → a single 1:1 copy. The wide middle spans many native
|
|
// widths → it TILES, matching retail's "fill-tile" + ImgTex::TileCSI rather
|
|
// than stretching one copy. (Same UV-repeat the chrome border already uses.)
|
|
DrawPiece(ctx, lt, 0f, capL, lw, h, clipW);
|
|
DrawPiece(ctx, mt, capL, midW, mw, h, clipW);
|
|
DrawPiece(ctx, rt, w - capR, capR, rw, h, clipW);
|
|
}
|
|
|
|
/// <summary>Draw a slice over local [<paramref name="pieceX"/>,
|
|
/// pieceX+<paramref name="pieceW"/>], with the texture repeating every
|
|
/// <paramref name="nativeW"/> px (UV-repeat — the UI texture is GL_REPEAT-wrapped).
|
|
/// Clipped so nothing past <paramref name="clipW"/> shows. For a cap (span == native)
|
|
/// this is one 1:1 copy; for the wide middle it tiles; a partial last copy is
|
|
/// UV-cropped.</summary>
|
|
private static void DrawPiece(
|
|
UiRenderContext ctx, uint tex, float pieceX, float pieceW, float nativeW, float h, float clipW)
|
|
{
|
|
if (tex == 0 || pieceW <= 0f || nativeW <= 0f) return;
|
|
float visibleW = MathF.Min(pieceW, clipW - pieceX);
|
|
if (visibleW <= 0f) return;
|
|
float u1 = visibleW / nativeW; // >1 ⇒ texture repeats (tiles); ≤1 ⇒ a partial copy
|
|
ctx.DrawSprite(tex, pieceX, 0f, visibleW, h, 0f, 0f, u1, 1f, Vector4.One);
|
|
}
|
|
}
|