The vitals cur/max numbers now render through the generic UiText widget — retail gmVitalsUI uses UIElement_Text for them, not a meter-internal label. VitalsController attaches a centered, non-interactive UiText child to each meter and stops the meter drawing its own label (UiMeter.Label -> null). New UiText.Centered draws the first line centered H+V with the SAME formula UiMeter's overlay used, so the numbers are pixel-identical — user-confirmed in the live client. This completes the D.2b widget-generalization pass: every chat + vitals widget is now built generically and registered to its retail Type (Button/Field*/Menu/Meter/Scrollbar/ Text), with thin find-by-id controllers. (*Field is controller-placed; Type 3 stays UiDatElement for chrome.) Divergence register: AP-37 vitals-numbers-via-UiMeter.Label clause retired. Full suite: 404 passed, 2 skipped, 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
98 lines
4.3 KiB
C#
98 lines
4.3 KiB
C#
using System;
|
|
using System.Numerics;
|
|
using AcDream.App.UI;
|
|
|
|
namespace AcDream.App.UI.Layout;
|
|
|
|
/// <summary>
|
|
/// Per-window controller for the vitals layout (LayoutDesc 0x2100006C).
|
|
/// Mirrors retail <c>gmVitalsUI::PostInit</c>: grab the three meter elements
|
|
/// by their dat element ids and bind live data providers (fill fraction + cur/max
|
|
/// text) to each. This is the ONLY per-window code in the whole importer — pure
|
|
/// data wiring, not graphics.
|
|
///
|
|
/// <para>The slice sprites + dat font on each <see cref="UiMeter"/> are already
|
|
/// set by <see cref="DatWidgetFactory"/> during tree construction; this controller
|
|
/// only binds the dynamic vitals data. Do not touch meter rendering fields here.</para>
|
|
///
|
|
/// <para>Element ids confirmed from
|
|
/// <c>docs/research/2026-06-15-layoutdesc-format.md §11</c>
|
|
/// (vitals window 0x2100006C dump).</para>
|
|
/// </summary>
|
|
public static class VitalsController
|
|
{
|
|
/// <summary>Dat element id for the Health meter (0x100000E6).</summary>
|
|
public const uint Health = 0x100000E6;
|
|
/// <summary>Dat element id for the Stamina meter (0x100000EC).</summary>
|
|
public const uint Stamina = 0x100000EC;
|
|
/// <summary>Dat element id for the Mana meter (0x100000EE).</summary>
|
|
public const uint Mana = 0x100000EE;
|
|
|
|
/// <summary>
|
|
/// Bind live vitals data providers to the Health, Stamina, and Mana meter
|
|
/// elements found in <paramref name="layout"/>. Any meter whose id is absent
|
|
/// from the layout is silently skipped — partial layouts (e.g. test fakes)
|
|
/// do not cause errors.
|
|
/// </summary>
|
|
/// <param name="layout">Imported vitals layout tree.</param>
|
|
/// <param name="healthPct">Provider returning Health fill fraction [0..1].</param>
|
|
/// <param name="staminaPct">Provider returning Stamina fill fraction [0..1].</param>
|
|
/// <param name="manaPct">Provider returning Mana fill fraction [0..1].</param>
|
|
/// <param name="healthText">Provider returning Health "cur/max" overlay text.</param>
|
|
/// <param name="staminaText">Provider returning Stamina "cur/max" overlay text.</param>
|
|
/// <param name="manaText">Provider returning Mana "cur/max" overlay text.</param>
|
|
public static void Bind(
|
|
ImportedLayout layout,
|
|
Func<float> healthPct,
|
|
Func<float> staminaPct,
|
|
Func<float> manaPct,
|
|
Func<string> healthText,
|
|
Func<string> staminaText,
|
|
Func<string> manaText)
|
|
{
|
|
BindMeter(layout, Health, healthPct, healthText);
|
|
BindMeter(layout, Stamina, staminaPct, staminaText);
|
|
BindMeter(layout, Mana, manaPct, manaText);
|
|
}
|
|
|
|
/// <summary>White cur/max numbers — matches the former <c>UiMeter.LabelColor</c> default.</summary>
|
|
private static readonly Vector4 NumberColor = new(1f, 1f, 1f, 1f);
|
|
|
|
private static void BindMeter(
|
|
ImportedLayout layout, uint id,
|
|
Func<float> pct,
|
|
Func<string> text)
|
|
{
|
|
// Silently skip if the id is absent — missing meters are not an error (partial layouts).
|
|
if (layout.FindElement(id) is not UiMeter m) return;
|
|
|
|
m.Fill = () => pct();
|
|
|
|
// Retail gmVitalsUI renders the cur/max as a real UIElement_Text centered over the
|
|
// bar — NOT a meter-internal label. Attach a centered UiText (non-interactive
|
|
// decoration) that fills + stretches with the meter, and stop the meter drawing its
|
|
// own label. UiText.Centered uses the SAME centering formula the meter's overlay did,
|
|
// so the numbers stay pixel-identical (locked by the visual gate).
|
|
m.Label = () => null;
|
|
|
|
var number = new UiText
|
|
{
|
|
Left = 0f, Top = 0f, Width = m.Width, Height = m.Height,
|
|
Anchors = AnchorEdges.Left | AnchorEdges.Top | AnchorEdges.Right | AnchorEdges.Bottom,
|
|
Centered = true,
|
|
DatFont = m.DatFont, // the same dat font the meter used for its label
|
|
ClickThrough = true, // decoration: no focus / selection / drag
|
|
AcceptsFocus = false,
|
|
IsEditControl = false,
|
|
CapturesPointerDrag = false,
|
|
LinesProvider = () =>
|
|
{
|
|
var s = text();
|
|
return string.IsNullOrEmpty(s)
|
|
? Array.Empty<UiText.Line>()
|
|
: new[] { new UiText.Line(s, NumberColor) };
|
|
},
|
|
};
|
|
m.AddChild(number);
|
|
}
|
|
}
|