diff --git a/src/AcDream.App/UI/Layout/VitalsController.cs b/src/AcDream.App/UI/Layout/VitalsController.cs
new file mode 100644
index 00000000..a455761a
--- /dev/null
+++ b/src/AcDream.App/UI/Layout/VitalsController.cs
@@ -0,0 +1,64 @@
+using System;
+
+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.
+///
+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);
+ }
+
+ private static void BindMeter(
+ ImportedLayout layout, uint id,
+ Func pct,
+ Func text)
+ {
+ if (layout.FindElement(id) is UiMeter m)
+ {
+ m.Fill = () => pct();
+ m.Label = () => text();
+ }
+ // Silently skip if the id is absent — missing meters are not an error.
+ }
+}
diff --git a/tests/AcDream.App.Tests/UI/Layout/VitalsBindingTests.cs b/tests/AcDream.App.Tests/UI/Layout/VitalsBindingTests.cs
new file mode 100644
index 00000000..8b430265
--- /dev/null
+++ b/tests/AcDream.App.Tests/UI/Layout/VitalsBindingTests.cs
@@ -0,0 +1,102 @@
+using AcDream.App.UI;
+using AcDream.App.UI.Layout;
+
+namespace AcDream.App.Tests.UI.Layout;
+
+///
+/// Unit tests for : verifies that the controller
+/// correctly maps element ids to UiMeter instances and wires the Fill / Label providers.
+/// No dats, no GL — pure data-wiring tests.
+///
+public class VitalsBindingTests
+{
+ // ── Test 1: Health meter Fill + Label providers are bound ─────────────────
+
+ [Fact]
+ public void Bind_SetsHealthMeterFillFromProvider()
+ {
+ var health = new UiMeter();
+ var layout = FakeLayout(("0x100000E6", health));
+ float hp = 0.42f;
+
+ VitalsController.Bind(layout,
+ healthPct: () => hp,
+ staminaPct: () => 1f,
+ manaPct: () => 1f,
+ healthText: () => "42/100",
+ staminaText: () => "",
+ manaText: () => "");
+
+ Assert.Equal(0.42f, health.Fill()!.Value);
+ Assert.Equal("42/100", health.Label());
+ }
+
+ // ── Test 2: All three meters wired to distinct providers ──────────────────
+
+ [Fact]
+ public void Bind_AllThreeMeters_EachBoundToOwnProvider()
+ {
+ var health = new UiMeter();
+ var stamina = new UiMeter();
+ var mana = new UiMeter();
+ var layout = FakeLayout(
+ ("0x100000E6", health),
+ ("0x100000EC", stamina),
+ ("0x100000EE", mana));
+
+ VitalsController.Bind(layout,
+ healthPct: () => 0.25f,
+ staminaPct: () => 0.50f,
+ manaPct: () => 0.75f,
+ healthText: () => "25/100",
+ staminaText: () => "50/100",
+ manaText: () => "75/100");
+
+ // Each meter should reflect its own provider, not another's.
+ Assert.Equal(0.25f, health.Fill()!.Value);
+ Assert.Equal("25/100", health.Label());
+
+ Assert.Equal(0.50f, stamina.Fill()!.Value);
+ Assert.Equal("50/100", stamina.Label());
+
+ Assert.Equal(0.75f, mana.Fill()!.Value);
+ Assert.Equal("75/100", mana.Label());
+ }
+
+ // ── Test 3: Missing meter ids are silently skipped (no throw) ─────────────
+
+ [Fact]
+ public void Bind_MissingMeterIds_DoesNotThrow()
+ {
+ // Only Health is present; Stamina and Mana are absent from the layout.
+ var health = new UiMeter();
+ var layout = FakeLayout(("0x100000E6", health));
+
+ // Should not throw even though Stamina/Mana are missing.
+ VitalsController.Bind(layout,
+ healthPct: () => 1f,
+ staminaPct: () => 1f,
+ manaPct: () => 1f,
+ healthText: () => "100/100",
+ staminaText: () => "100/100",
+ manaText: () => "100/100");
+
+ // Health was present — it should be wired.
+ Assert.Equal(1f, health.Fill()!.Value);
+ }
+
+ // ── Helpers ───────────────────────────────────────────────────────────────
+
+ private static ImportedLayout FakeLayout(params (string idHex, UiElement e)[] items)
+ {
+ var dict = new Dictionary();
+ var root = new UiPanel();
+ foreach (var (idHex, e) in items)
+ {
+ uint id = Convert.ToUInt32(idHex, 16);
+ root.AddChild(e);
+ dict[id] = e;
+ }
+ return new ImportedLayout(root, dict);
+ }
+}