using System;
using System.Numerics;
using AcDream.App.UI;
namespace AcDream.App.UI.Layout;
///
/// Per-window controller for the vitals layout (LayoutDesc 0x2100006C).
/// Mirrors retail gmVitalsUI::PostInit: 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.
///
/// The slice sprites + dat font on each are already
/// set by during tree construction; this controller
/// only binds the dynamic vitals data. Do not touch meter rendering fields here.
///
/// Element ids confirmed from
/// docs/research/2026-06-15-layoutdesc-format.md §11
/// (vitals window 0x2100006C dump).
///
public static class VitalsController
{
/// Dat element id for the Health meter (0x100000E6).
public const uint Health = 0x100000E6;
/// Dat element id for the Stamina meter (0x100000EC).
public const uint Stamina = 0x100000EC;
/// Dat element id for the Mana meter (0x100000EE).
public const uint Mana = 0x100000EE;
///
/// Bind live vitals data providers to the Health, Stamina, and Mana meter
/// elements found in . Any meter whose id is absent
/// from the layout is silently skipped — partial layouts (e.g. test fakes)
/// do not cause errors.
///
/// Imported vitals layout tree.
/// Provider returning Health fill fraction [0..1].
/// Provider returning Stamina fill fraction [0..1].
/// Provider returning Mana fill fraction [0..1].
/// Provider returning Health "cur/max" overlay text.
/// Provider returning Stamina "cur/max" overlay text.
/// Provider returning Mana "cur/max" overlay text.
public static void Bind(
ImportedLayout layout,
Func healthPct,
Func staminaPct,
Func manaPct,
Func healthText,
Func staminaText,
Func manaText)
{
BindMeter(layout, Health, healthPct, healthText);
BindMeter(layout, Stamina, staminaPct, staminaText);
BindMeter(layout, Mana, manaPct, manaText);
}
/// White cur/max numbers — matches the former UiMeter.LabelColor default.
private static readonly Vector4 NumberColor = new(1f, 1f, 1f, 1f);
private static void BindMeter(
ImportedLayout layout, uint id,
Func pct,
Func 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()
: new[] { new UiText.Line(s, NumberColor) };
},
};
m.AddChild(number);
}
}