diff --git a/.gitignore b/.gitignore
index 904fdf9c..1731f2eb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,7 +23,3 @@ references/
# Claude Code session state
.claude/
launch.log
-launch-*.log
-
-# ImGui auto-saved window/docking state (per-user, not source)
-imgui.ini
diff --git a/AcDream.slnx b/AcDream.slnx
index 1cf8f243..e7fd39ac 100644
--- a/AcDream.slnx
+++ b/AcDream.slnx
@@ -6,8 +6,6 @@
-
-
@@ -16,6 +14,5 @@
-
diff --git a/CLAUDE.md b/CLAUDE.md
index 96449ed3..84c1d809 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -25,14 +25,13 @@ The codebase is organized by layer (see architecture doc). Current phase
state lives in memory (`memory/project_*.md`), plans in `docs/plans/`,
research in `docs/research/`.
-**UI strategy:** three-layer split — swappable backend (ImGui.NET +
-`Silk.NET.OpenGL.Extensions.ImGui` for Phase D.2a short-term, custom
-retail-look toolkit for D.2b later) / stable `AcDream.UI.Abstractions`
-layer (ViewModels + Commands + `IPanel` / `IPanelRenderer`) / unchanged
-game state. **All plugin-facing UI targets `AcDream.UI.Abstractions` —
-never import a backend namespace from a panel.** Full design:
-`docs/plans/2026-04-24-ui-framework.md`. Memory crib:
-`memory/project_ui_architecture.md`.
+**UI strategy:** three-layer split — swappable backend (Hexa.NET.ImGui for
+Phase D.2a short-term, custom retail-look toolkit for D.2b later) /
+stable `AcDream.UI.Abstractions` layer (ViewModels + Commands + `IPanel`
+/ `IPanelRenderer`) / unchanged game state. **All plugin-facing UI
+targets `AcDream.UI.Abstractions` — never import a backend namespace
+from a panel.** Full design: `docs/plans/2026-04-24-ui-framework.md`.
+Memory crib: `memory/project_ui_architecture.md`.
## How to operate
diff --git a/docs/ISSUES.md b/docs/ISSUES.md
index 5f2c01a9..38d52ec9 100644
--- a/docs/ISSUES.md
+++ b/docs/ISSUES.md
@@ -135,29 +135,6 @@ Copy this block when adding a new issue:
---
-## #5 — VitalsPanel stamina/mana bars always null (absolute values not stored)
-
-**Status:** OPEN
-**Severity:** LOW (cosmetic — HP bar already works; stam/mana would be a nice-to-have)
-**Filed:** 2026-04-25
-**Component:** ui / net / player-state
-
-**Description:** Phase D.2a shipped `VitalsVM` with `StaminaPercent` / `ManaPercent` returning `float?` null. `VitalsPanel` already renders an HP progress bar from `CombatState.GetHealthPercent(localGuid)` because per-entity health is tracked from combat notifications. Stamina and mana are absolute values and only arrive in `PlayerDescription (0x0013)` — which we currently parse then discard. Result: the Vitals window shows HP only.
-
-**Root cause / status:** We need a `LocalPlayerState` Core class (analogous to `CombatState` but scoped to the local player) that retains parsed `PlayerDescription` fields — at minimum: `CurrentStamina` + `MaxStamina` + `CurrentMana` + `MaxMana`. `AppraiseInfoParser.CreatureProfile` already has the shape for these values; we just don't persist them.
-
-**Files:**
-- `src/AcDream.Core.Net/Parsers/PlayerDescriptionParser.cs` — parses then discards (verify path)
-- `src/AcDream.Core.Net/Parsers/AppraiseInfoParser.cs` — has `CreatureProfile` with absolute values
-- `src/AcDream.UI.Abstractions/Panels/Vitals/VitalsVM.cs` — `StaminaPercent` / `ManaPercent` would divide `LocalPlayerState.Current*` by `Max*`
-- `src/AcDream.App/Rendering/GameWindow.cs` — construct `LocalPlayerState`, hand to `VitalsVM`, wire into event dispatch
-
-**Research:** none needed — wire-level field positions are already decoded in `PlayerDescriptionParser`.
-
-**Acceptance:** With `ACDREAM_DEVTOOLS=1`, the Vitals window shows three progress bars (HP / Stamina / Mana) that update when the server sends `PlayerDescription` or any delta event (`UpdateHealth`, `UpdateStamina`, `UpdateMana`).
-
----
-
# Recently closed
*(none yet — move DONE items here with closed-date + commit SHA)*
diff --git a/docs/architecture/acdream-architecture.md b/docs/architecture/acdream-architecture.md
index 80536a17..e5f7f856 100644
--- a/docs/architecture/acdream-architecture.md
+++ b/docs/architecture/acdream-architecture.md
@@ -74,8 +74,7 @@ designed 2026-04-24. Full design: `docs/plans/2026-04-24-ui-framework.md`.
```
┌─────────────────────────────────────────────────────────────┐
│ UI BACKEND (swappable) │
-│ ImGui.NET + Silk.NET.OpenGL.Extensions.ImGui │
-│ (Phase D.2a, short-term) │
+│ Hexa.NET.ImGui (Phase D.2a, short-term) │
│ or custom retail-look toolkit (Phase D.2b, later) │
├─────────────────────────────────────────────────────────────┤
│ AcDream.UI.Abstractions (stable contract) │
diff --git a/docs/plans/2026-04-11-roadmap.md b/docs/plans/2026-04-11-roadmap.md
index aabe5685..15595904 100644
--- a/docs/plans/2026-04-11-roadmap.md
+++ b/docs/plans/2026-04-11-roadmap.md
@@ -46,7 +46,6 @@
| G.1+ | Full sky visuals + weather + dynamic-light shader — SkyDescLoader parses Region 0x13000000 dat keyframes with retail fog fields (start/end/mode); WeatherSystem picks Clear/Overcast/Rain/Snow/Storm deterministically per in-game day with 10s fade; SkyRenderer draws far-plane-1e6 celestial meshes with UV scroll; SceneLightingUbo binds at std140 location=1 with 8 Light slots + fog + lightning flash; terrain.vert + mesh.frag + mesh_instanced.frag + sky.frag all consume the shared UBO; LightingHookSink auto-registers Setup.Lights per entity + flips IsLit on SetLightHook; ParticleRenderer renders rain/snow billboards; F7 cycles day time override, F10 cycles weather; WorldSession surfaces server time via ServerTimeUpdated (ConnectRequest + TimeSync flag) | Tests ✓ |
| H.1 | Chat wire layer — Talk (0x0015) / Tell (0x005D) / ChatChannel (0x0147) outbound, HearSpeech (0x02BB local + 0x02BC ranged) inbound, ChatLog ring buffer with adapters for every chat source | Tests ✓ |
| Glue | GameEventWiring.WireAll — single-call registration mapping parsed GameEvents → Core state classes (ChatLog, CombatState, Spellbook, ItemRepository); GameWindow exposes state classes + wires them to live session | Tests ✓ |
-| D.2a | UI scaffold — `AcDream.UI.Abstractions` stable contract (`IPanel` / `IPanelHost` / `IPanelRenderer` / `ICommandBus` + `VitalsVM` / `VitalsPanel`); `AcDream.UI.ImGui` backend on ImGui.NET + `Silk.NET.OpenGL.Extensions.ImGui` (pivoted from Hexa.NET.ImGui on 2026-04-25 — Hexa's native OpenGL3 backend resolves GL via GLFW/SDL and crashed 0xC0000005 without them); VitalsPanel wired into GameWindow behind `ACDREAM_DEVTOOLS=1` with `ImGui.WantCaptureKeyboard` WASD suppression. 11 new tests. | Live ✓ |
Plus polish that doesn't get its own phase number:
- FlyCamera default speed lowered + Shift-to-boost
@@ -127,11 +126,9 @@ Plus polish that doesn't get its own phase number:
> [`docs/plans/2026-04-24-ui-framework.md`](2026-04-24-ui-framework.md)
> for the full design. Short version:
>
-> 1. **D.2a — ImGui as the short-term backend.** Wire up in days,
+> 1. **D.2a — Hexa.NET.ImGui as the short-term backend.** Wire up in days,
> iterate game logic (chat-send, inventory actions, vitals HUD reading
-> real state) in weeks. Looks like a debugger; that's fine. *(Shipped
-> 2026-04-25 on ImGui.NET + `Silk.NET.OpenGL.Extensions.ImGui` after a
-> day-one pivot away from Hexa.NET.ImGui; see shipped table.)*
+> real state) in weeks. Looks like a debugger; that's fine.
> 2. **Stable `AcDream.UI.Abstractions` layer** — ViewModels + Commands +
> `IPanel` / `IPanelRenderer` interfaces. Backend-agnostic. Plugin API
> publishes against this layer and never sees ImGui.
@@ -145,7 +142,7 @@ Plus polish that doesn't get its own phase number:
**Sub-pieces:**
- **D.1 — 2D ortho overlay + font rendering.** ✅ SHIPPED 2026-04-17 as the dev-facing debug overlay (StbTrueTypeSharp system-font atlas + `TextRenderer` + `DebugOverlay`).
-- **✓ SHIPPED — D.2a — ImGui scaffold + `AcDream.UI.Abstractions` layer.** Shipped 2026-04-25. Wires ImGui as the short-term backend behind `ACDREAM_DEVTOOLS=1`. Defines `IPanel` / `IPanelHost` / `IPanelRenderer` / `ICommandBus` + the first ViewModel (`VitalsVM`) in the new `AcDream.UI.Abstractions` project. First real panel: `VitalsPanel` reading HP from `CombatState.GetHealthPercent`. **Backend pivoted Hexa.NET.ImGui → ImGui.NET + `Silk.NET.OpenGL.Extensions.ImGui` during integration** — Hexa's native OpenGL3 backend does its own GL function resolution via GLFW/SDL and crashed with `0xC0000005` in `ImGuiImplOpenGL3.InitNative` against Silk.NET (no GLFW/SDL present). The Silk.NET extension is purpose-built for this scenario and is the `ImGui.NET` mitigation path that `docs/plans/2026-04-24-ui-framework.md` already called out as a "one-morning operation". Stam/Mana return `float?` null in D.2a because absolute values need `LocalPlayerState` + `PlayerDescription (0x0013)` parsing (filed post-D.2a). 11 new `AcDream.UI.Abstractions.Tests` green.
+- **D.2a — Hexa.NET.ImGui scaffold + `AcDream.UI.Abstractions` layer.** NEW pre-piece introduced 2026-04-24. Wires Hexa.NET.ImGui as the short-term backend behind `ACDREAM_DEVTOOLS=1`. Defines `IPanel` / `IPanelHost` / `IPanelRenderer` / `ICommandBus` + the first ViewModels (`VitalsVM` etc.) in the new `AcDream.UI.Abstractions` project. First real panel: `VitalsPanel` reading HP/stam/mana from `IGameState`. This is what gets game-logic iteration moving; looks like a debugger, acceptable.
- **D.2b — Custom retail-look backend.** Implements the same `IPanel` / `IPanelRenderer` contracts using a custom retained-mode toolkit sourced from retail dat assets. Requires D.2a shipped. Panels get reskinned one at a time; ImGui stays as the `ACDREAM_DEVTOOLS=1` overlay forever. The original 2026-04-17 scaffold research (`UiRoot` / `UiElement` / `UiPanel` / `UiHost` + retail event codes + focus / drag-drop state machine + `WorldMouseFallThrough`) is the implementation foundation here — see `docs/research/retail-ui/`.
- **D.3 — AcFont from portal.dat.** Replace stb_truetype system font with retail `Font` DBObjs (`0x40000000..0x40000FFF`) baked from `RenderSurface` source sheets — see research slice 03 §4. Preserves retail visual identity. **(D.2b dependency — needs the custom renderer.)**
- **D.4 — Dat sprites + 9-slice panel backgrounds.** Load `RenderSurface` (`0x06xxxxxx`) as GL textures; add `DrawSprite` to `UiRenderContext`. Enables retail panel art. **(D.2b dependency.)**
diff --git a/docs/plans/2026-04-24-ui-framework.md b/docs/plans/2026-04-24-ui-framework.md
index e6fce264..85d8a41c 100644
--- a/docs/plans/2026-04-24-ui-framework.md
+++ b/docs/plans/2026-04-24-ui-framework.md
@@ -1,51 +1,13 @@
# UI framework plan
-**Date:** 2026-04-24 (design), shipped 2026-04-25
-**Status:** **Phase D.2a shipped** — `AcDream.UI.Abstractions` + ImGui backend
-+ `VitalsPanel` gated on `ACDREAM_DEVTOOLS=1`. Backend pivoted from
-`Hexa.NET.ImGui` to `ImGui.NET` + `Silk.NET.OpenGL.Extensions.ImGui` during
-first-light integration — see the pivot note below. Phase D.2b (custom
-retail-look backend) remains design-only.
+**Date:** 2026-04-24
+**Status:** design — not yet implemented
**Owner:** lead engineer (erik) + Claude
Captures the UI strategy agreed via discussion on 2026-04-24. Documents
the choices AND the alternatives considered so future sessions can
re-evaluate with the same context.
-## 2026-04-25 pivot: Hexa.NET.ImGui → ImGui.NET
-
-The original choice (documented below) was `Hexa.NET.ImGui` +
-`Hexa.NET.ImGui.Backends.OpenGL3`. It did not survive first-light
-integration:
-
-- First launch with Hexa's backend crashed with `0xC0000005` inside
- `Hexa.NET.ImGui.Backends.OpenGL3.ImGuiImplOpenGL3.InitNative`.
-- Root cause: Hexa's native OpenGL3 backend does its own GL function
- resolution, looking up symbols via GLFW or SDL. Silk.NET uses neither,
- so the resolved function pointers were null and the native code
- dereferenced them on init.
-- Hexa's Silk.NET examples rely on GLFW being co-loaded (its default
- on Hexa's own scenes) — not applicable here.
-
-**Mitigation path** was already written into this doc (§"What we give
-up": *"switching to ImGui.NET later is a one-morning operation if Hexa
-misbehaves"*) and taken:
-
-- Packages swapped → `ImGui.NET 1.91.6.1` + `Silk.NET.OpenGL.Extensions.ImGui 2.23.0`.
-- `Silk.NET.OpenGL.Extensions.ImGui.ImGuiController` handles the whole
- integration (GL backend init against the Silk.NET GL binding,
- keyboard + mouse IO event subscription). No hand-written input
- bridge needed.
-- `ImGuiBootstrapper` is a ~10-line `IDisposable` wrapping the
- `ImGuiController` instance. `ImGuiPanelRenderer` wraps `ImGuiNET.ImGui.*`.
-- Boundary discipline preserved — panels never import `ImGuiNET`
- directly; they only use `IPanelRenderer`. The backend swap is
- invisible above the abstraction layer, as designed.
-
-Sections below from §"Choice: Hexa.NET.ImGui" onward are kept as the
-historical design reasoning. They remain useful if we ever re-evaluate
-native AOT / upstream-tracking tradeoffs.
-
## Goal
acdream needs a playable game UI: chat, vitals HUD, inventory, character
diff --git a/docs/research/retail-ui/00-master-synthesis.md b/docs/research/retail-ui/00-master-synthesis.md
index 0dbbf771..7882a9cb 100644
--- a/docs/research/retail-ui/00-master-synthesis.md
+++ b/docs/research/retail-ui/00-master-synthesis.md
@@ -1,12 +1,10 @@
# Retail AC Client GUI — Master Synthesis
-> **Scope note (2026-04-24, updated 2026-04-25):** This document
-> describes retail's Keystone UI toolkit — it is the research foundation
-> for **Phase D.2b (custom retail-look backend)**, not Phase D.2a
-> (shipped ImGui scaffold, `AcDream.UI.Abstractions` + ImGui.NET +
-> `Silk.NET.OpenGL.Extensions.ImGui` + `VitalsPanel`). When reading this
-> for implementation guidance, assume D.2a has shipped a working
-> `AcDream.UI.Abstractions` layer (`IPanel`, `IPanelRenderer`,
+> **Scope note (2026-04-24):** This document describes retail's Keystone
+> UI toolkit — it is the research foundation for **Phase D.2b (custom
+> retail-look backend)**, not **Phase D.2a (Hexa.NET.ImGui scaffold)**.
+> When reading this for implementation guidance, assume D.2a has shipped
+> a working `AcDream.UI.Abstractions` layer (`IPanel`, `IPanelRenderer`,
> ViewModels, Commands) and you are building the custom retained-mode
> toolkit that implements the same contracts using dat-sourced fonts /
> sprites / cursors. See `docs/plans/2026-04-24-ui-framework.md` for the
diff --git a/memory/project_ui_architecture.md b/memory/project_ui_architecture.md
index bf5e4ad6..2d6240de 100644
--- a/memory/project_ui_architecture.md
+++ b/memory/project_ui_architecture.md
@@ -15,14 +15,10 @@
└─────────────────────────────────────────┘
```
-- **UI backend** (bottom swap axis): **`ImGui.NET` + `Silk.NET.OpenGL.Extensions.ImGui`** for Phase D.2a
+- **UI backend** (bottom swap axis): `Hexa.NET.ImGui` for Phase D.2a
(short-term, debugger-look, validates game logic fast). Custom
retail-look toolkit for Phase D.2b (long-term, uses dat assets).
ImGui stays **forever** as the `ACDREAM_DEVTOOLS=1` overlay.
- (Pivoted from `Hexa.NET.ImGui` on 2026-04-25 — Hexa's native OpenGL3
- backend resolves GL via GLFW/SDL internally and crashed `0xC0000005`
- against Silk.NET; the Silk.NET extension is purpose-built for this
- stack.)
- **ViewModels + Commands** (the stable contract): per-panel data
records (`VitalsVM`, `InventoryVM`, `ChatVM`, …) and action records
(`UseItemCmd`, `SendChatCmd`, `CastSpellCmd`, …). Lives in
@@ -36,19 +32,16 @@
- `src/AcDream.UI.Abstractions/` — `IPanel`, `IPanelHost`,
`IPanelRenderer`, `ICommandBus`, all ViewModels + Commands. Backend-
agnostic.
-- `src/AcDream.UI.ImGui/` — `ImGui.NET` + `Silk.NET.OpenGL.Extensions.ImGui`
- implementation of `IPanelRenderer` + ImGui bootstrap. Phase D.2a.
- `ImGuiController` (from the Silk.NET extension) handles GL backend +
- input event subscription; `ImGuiBootstrapper` is a thin IDisposable
- wrapper; `ImGuiPanelRenderer` wraps the widgets `IPanelRenderer` needs.
+- `src/AcDream.UI.ImGui/` — Hexa.NET.ImGui-based implementation of
+ `IPanelRenderer` + ImGui bootstrap. Phase D.2a.
- `src/AcDream.UI.Retail/` (later) — custom retained-mode toolkit using
dat assets, same `IPanelRenderer` contract. Phase D.2b.
## Hard rules
1. **No panel references a backend namespace.** If a panel imports
- `ImGuiNET` / `Silk.NET.OpenGL.Extensions.ImGui` or a custom-toolkit
- widget class directly, it's a bug — extend `IPanelRenderer` instead.
+ `Hexa.NET.ImGui` or a custom-toolkit widget class directly, it's a
+ bug.
2. **Plugin API targets the abstraction layer only.** Plugins define
`IPanel` instances; they never see which backend draws them.
3. **Features that only ImGui can express → not in `IPanelRenderer`.**
diff --git a/src/AcDream.App/AcDream.App.csproj b/src/AcDream.App/AcDream.App.csproj
index 6fb3af04..277ae99c 100644
--- a/src/AcDream.App/AcDream.App.csproj
+++ b/src/AcDream.App/AcDream.App.csproj
@@ -24,8 +24,6 @@
-
-
diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs
index ab4479f3..cbd4b27f 100644
--- a/src/AcDream.App/Rendering/GameWindow.cs
+++ b/src/AcDream.App/Rendering/GameWindow.cs
@@ -281,14 +281,6 @@ public sealed class GameWindow : IDisposable
public readonly AcDream.Core.Spells.Spellbook SpellBook = new();
public readonly AcDream.Core.Items.ItemRepository Items = new();
- // Phase D.2a — ImGui devtools UI overlay. Null unless ACDREAM_DEVTOOLS=1.
- // See docs/plans/2026-04-24-ui-framework.md for the staged UI strategy.
- private AcDream.UI.ImGui.ImGuiBootstrapper? _imguiBootstrap;
- private AcDream.UI.ImGui.ImGuiPanelHost? _panelHost;
- private AcDream.UI.Abstractions.Panels.Vitals.VitalsVM? _vitalsVm;
- private static readonly bool DevToolsEnabled =
- Environment.GetEnvironmentVariable("ACDREAM_DEVTOOLS") == "1";
-
// Phase G.1-G.2 world lighting/time state.
public readonly AcDream.Core.World.WorldTimeService WorldTime =
new AcDream.Core.World.WorldTimeService(
@@ -878,35 +870,6 @@ public sealed class GameWindow : IDisposable
}
}
- // Phase D.2a — ImGui devtools overlay. Zero cost when the env var
- // isn't set: no context creation, no per-frame branches hit.
- // See docs/plans/2026-04-24-ui-framework.md + memory/project_ui_architecture.md.
- if (DevToolsEnabled)
- {
- try
- {
- _imguiBootstrap = new AcDream.UI.ImGui.ImGuiBootstrapper(_gl!, _window!, _input!);
- _panelHost = new AcDream.UI.ImGui.ImGuiPanelHost();
-
- // VitalsVM: GUID=0 at construction; set later at EnterWorld
- // (see the _playerServerGuid assignment path). Pre-login the
- // HP bar just reads 1.0 (safe default) — harmless.
- _vitalsVm = new AcDream.UI.Abstractions.Panels.Vitals.VitalsVM(Combat);
- _panelHost.Register(
- new AcDream.UI.Abstractions.Panels.Vitals.VitalsPanel(_vitalsVm));
-
- Console.WriteLine("devtools: ImGui panel host ready (VitalsPanel registered)");
- }
- catch (Exception ex)
- {
- Console.WriteLine($"devtools: ImGui init failed: {ex.Message} — devtools disabled");
- _imguiBootstrap?.Dispose();
- _imguiBootstrap = null;
- _panelHost = null;
- _vitalsVm = null;
- }
- }
-
uint centerLandblockId = 0xA9B4FFFFu;
Console.WriteLine($"loading world view centered on 0x{centerLandblockId:X8}");
@@ -1186,7 +1149,6 @@ public sealed class GameWindow : IDisposable
var chosen = _liveSession.Characters.Characters[0];
_playerServerGuid = chosen.Id; // Phase B.2: store for Tab-key player-mode entry
- _vitalsVm?.SetLocalPlayerGuid(chosen.Id); // Phase D.2a — devtools HP bar tracks this guid
_worldState.MarkPersistent(chosen.Id); // player entity survives landblock unloads
Console.WriteLine($"live: entering world as 0x{chosen.Id:X8} {chosen.Name}");
_liveSession.EnterWorld(user, characterIndex: 0);
@@ -3540,13 +3502,6 @@ public sealed class GameWindow : IDisposable
var kb = _input.Keyboards[0];
- // Phase D.2a — suppress game-side WASD / interaction polling when
- // ImGui has keyboard focus (e.g. a text field is active). Without
- // this, typing "walk" into a chat field would actually walk.
- bool suppressGameInput =
- DevToolsEnabled && ImGuiNET.ImGui.GetIO().WantCaptureKeyboard;
- if (suppressGameInput) return;
-
if (_cameraController.IsFlyMode)
{
_cameraController.Fly.Update(
@@ -3754,12 +3709,6 @@ public sealed class GameWindow : IDisposable
_gl!.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
- // Phase D.2a — begin ImGui frame. Paired with the Render() call
- // after the scene draws (below). ImGuiController.Update()
- // consumes buffered Silk.NET input events and calls ImGui.NewFrame.
- if (DevToolsEnabled && _imguiBootstrap is not null)
- _imguiBootstrap.BeginFrame((float)deltaSeconds);
-
// Phase 6.4: advance per-entity animation playback before drawing
// so the renderer always sees the up-to-date per-part transforms.
if (_animatedEntities.Count > 0)
@@ -4053,20 +4002,6 @@ public sealed class GameWindow : IDisposable
}
}
- // Phase D.2a — end ImGui frame. Runs AFTER all scene + debug draws
- // so ImGui composites on top. ImGuiController save/restores the
- // GL state it touches (blend, scissor, VAO, shader, texture); any
- // state not in its save-list (e.g. GL_FRAMEBUFFER_SRGB, unused
- // today) would need manual protection.
- if (DevToolsEnabled && _imguiBootstrap is not null && _panelHost is not null)
- {
- var ctx = new AcDream.UI.Abstractions.PanelContext(
- (float)deltaSeconds,
- AcDream.UI.Abstractions.NullCommandBus.Instance);
- _panelHost.RenderAll(ctx);
- _imguiBootstrap.Render();
- }
-
// Update the window title with performance stats every ~0.5s.
_perfAccum += deltaSeconds;
_perfFrameCount++;
diff --git a/src/AcDream.UI.Abstractions/AcDream.UI.Abstractions.csproj b/src/AcDream.UI.Abstractions/AcDream.UI.Abstractions.csproj
deleted file mode 100644
index 0fc070c7..00000000
--- a/src/AcDream.UI.Abstractions/AcDream.UI.Abstractions.csproj
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
- net10.0
- enable
- enable
- latest
- true
-
-
-
-
-
diff --git a/src/AcDream.UI.Abstractions/ICommandBus.cs b/src/AcDream.UI.Abstractions/ICommandBus.cs
deleted file mode 100644
index 71742969..00000000
--- a/src/AcDream.UI.Abstractions/ICommandBus.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-namespace AcDream.UI.Abstractions;
-
-///
-/// Publishes user-intent commands from panels to the systems that handle
-/// them (WorldSession, ChatService, Inventory, ...). Panels never touch
-/// those systems directly — they a record
-/// and the bus dispatches.
-///
-///
-/// D.2a scaffolding: is the default wire-up
-/// — commands are accepted but dropped. Real routing lands alongside
-/// chat and inventory (Sprint 2 of the UI plan) when we actually need
-/// commands flowing server-ward.
-///
-///
-public interface ICommandBus
-{
- ///
- /// Publish a command record. The bus routes by runtime type via
- /// registered handlers. Never blocks; handlers run on the publish
- /// thread today (render thread for panel-triggered commands).
- ///
- void Publish(T command) where T : notnull;
-}
diff --git a/src/AcDream.UI.Abstractions/IPanel.cs b/src/AcDream.UI.Abstractions/IPanel.cs
deleted file mode 100644
index 5a4a37f7..00000000
--- a/src/AcDream.UI.Abstractions/IPanel.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-namespace AcDream.UI.Abstractions;
-
-///
-/// A UI panel — chat window, inventory, vitals HUD, character sheet, etc.
-/// Panels are backend-agnostic: they only call into
-/// primitives, never reach through to a specific UI library (Hexa.NET.ImGui
-/// in Phase D.2a, a custom retail-look toolkit in Phase D.2b).
-///
-///
-/// Hard rule: no using Hexa.NET.ImGui inside a panel file. If a
-/// widget needs a feature the abstraction doesn't expose, extend
-/// ; do not import the backend. See
-/// docs/plans/2026-04-24-ui-framework.md.
-///
-///
-public interface IPanel
-{
- /// Stable, globally-unique identifier. Convention: acdream.{name}.
- string Id { get; }
-
- /// Human-readable window title shown in the chrome of the panel.
- string Title { get; }
-
- ///
- /// Whether the panel is currently visible. Backends read this per frame;
- /// panels may mutate it in response to their own close-button handling.
- ///
- bool IsVisible { get; set; }
-
- ///
- /// Draw the panel for one frame. Called by
- /// on the render thread once ImGui's (or the future custom backend's)
- /// frame has begun. Panels issue drawing calls through
- /// and publish user-intent actions through ..
- ///
- void Render(PanelContext ctx, IPanelRenderer renderer);
-}
diff --git a/src/AcDream.UI.Abstractions/IPanelHost.cs b/src/AcDream.UI.Abstractions/IPanelHost.cs
deleted file mode 100644
index bbd685f7..00000000
--- a/src/AcDream.UI.Abstractions/IPanelHost.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-namespace AcDream.UI.Abstractions;
-
-///
-/// Owns the set of live s and drives per-frame draw
-/// dispatch. The backend (Hexa.NET.ImGui in D.2a, custom in D.2b) implements
-/// this; GameWindow creates one at startup and registers panels.
-///
-///
-/// Does not call ImGui.NewFrame / ImGui.Render — those
-/// belong to the caller so GL-state ownership is unambiguous. Caller pattern:
-///
-///
-///
-/// // per frame, render thread
-/// inputBridge.BeginFrame(size, dt);
-/// ImGui.NewFrame();
-/// panelHost.RenderAll(ctx);
-/// ImGui.Render();
-/// ImGuiImplOpenGL3.RenderDrawData(ImGui.GetDrawData());
-///
-///
-public interface IPanelHost
-{
- /// Register a panel for per-frame rendering. Idempotent by .
- void Register(IPanel panel);
-
- /// Remove the panel with the matching id. No-op if not present.
- void Unregister(string panelId);
-
- ///
- /// Iterate every visible panel and call . Call
- /// order within a frame is the registration order; panels with
- /// set to false are skipped entirely.
- ///
- void RenderAll(PanelContext ctx);
-}
diff --git a/src/AcDream.UI.Abstractions/IPanelRenderer.cs b/src/AcDream.UI.Abstractions/IPanelRenderer.cs
deleted file mode 100644
index 93e25847..00000000
--- a/src/AcDream.UI.Abstractions/IPanelRenderer.cs
+++ /dev/null
@@ -1,43 +0,0 @@
-namespace AcDream.UI.Abstractions;
-
-///
-/// Drawing primitives exposed to panels. The only API panels use to
-/// emit pixels. The ImGui backend maps these straight onto ImGui calls; the
-/// later custom retail-look backend will map the same primitives onto its
-/// own retained-mode toolkit using retail dat-sourced fonts / sprites.
-///
-///
-/// Keep this interface small and retail-friendly. If a widget requires a
-/// feature the custom backend couldn't express with dat assets, don't add
-/// it — find a different widget shape that both backends can satisfy.
-///
-///
-public interface IPanelRenderer
-{
- ///
- /// Begin a top-level window. Matches retail's root UiPanel +
- /// ImGui's Begin. Returns false if the window is collapsed
- /// — the caller must still call to balance.
- ///
- bool Begin(string title);
-
- /// Close the most recent .
- void End();
-
- /// Draw a single line of text. No formatting / markdown.
- void Text(string text);
-
- /// Keep the next widget on the same line as the previous one.
- void SameLine();
-
- /// Horizontal rule separator.
- void Separator();
-
- ///
- /// A filled progress bar.
- /// is clamped by the backend to [0, 1].
- /// is the pixel width of the full bar.
- /// is optional text (e.g. "54%") rendered on top.
- ///
- void ProgressBar(float fraction, float width, string? overlay = null);
-}
diff --git a/src/AcDream.UI.Abstractions/NullCommandBus.cs b/src/AcDream.UI.Abstractions/NullCommandBus.cs
deleted file mode 100644
index 2c111ea8..00000000
--- a/src/AcDream.UI.Abstractions/NullCommandBus.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-namespace AcDream.UI.Abstractions;
-
-///
-/// No-op . Accepts any published command and
-/// discards it. Used as the default in D.2a until chat / inventory panels
-/// need real command routing.
-///
-public sealed class NullCommandBus : ICommandBus
-{
- /// Shared singleton — the bus is stateless.
- public static readonly NullCommandBus Instance = new();
-
- private NullCommandBus() { }
-
- ///
- public void Publish(T command) where T : notnull
- {
- // Intentionally empty. Panel-emitted commands in D.2a are
- // read-only diagnostics; nothing routes server-ward yet.
- }
-}
diff --git a/src/AcDream.UI.Abstractions/PanelContext.cs b/src/AcDream.UI.Abstractions/PanelContext.cs
deleted file mode 100644
index 37caba1d..00000000
--- a/src/AcDream.UI.Abstractions/PanelContext.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-namespace AcDream.UI.Abstractions;
-
-///
-/// Per-frame context passed to each call.
-/// Struct + record for zero-allocation per frame. Add fields here as new
-/// capabilities become panel-facing — e.g. a future IGameState
-/// handle once we need richer data than individual ViewModels can carry.
-///
-///
-/// Carried by value; cheap. Passed per-render; do not cache across frames.
-///
-///
-public readonly record struct PanelContext(
- float DeltaSeconds,
- ICommandBus Commands);
diff --git a/src/AcDream.UI.Abstractions/Panels/Vitals/VitalsPanel.cs b/src/AcDream.UI.Abstractions/Panels/Vitals/VitalsPanel.cs
deleted file mode 100644
index 5040c7ea..00000000
--- a/src/AcDream.UI.Abstractions/Panels/Vitals/VitalsPanel.cs
+++ /dev/null
@@ -1,69 +0,0 @@
-namespace AcDream.UI.Abstractions.Panels.Vitals;
-
-///
-/// First real UI panel — shows the local player's vitals as progress bars.
-/// Backend-agnostic; renders exclusively through
-/// so the same file works under Hexa.NET.ImGui (D.2a) and the future custom
-/// retail-look toolkit (D.2b).
-///
-///
-/// D.2a shows only HP (percent). /
-/// return null until a
-/// LocalPlayerState is wired (follow-up issue). When they start
-/// returning non-null, this panel picks them up automatically.
-///
-///
-public sealed class VitalsPanel : IPanel
-{
- private const float BarWidth = 200f;
-
- private readonly VitalsVM _vm;
-
- public VitalsPanel(VitalsVM vm)
- {
- _vm = vm ?? throw new ArgumentNullException(nameof(vm));
- }
-
- ///
- public string Id => "acdream.vitals";
-
- ///
- public string Title => "Vitals";
-
- ///
- public bool IsVisible { get; set; } = true;
-
- ///
- public void Render(PanelContext ctx, IPanelRenderer renderer)
- {
- if (!renderer.Begin(Title))
- {
- renderer.End();
- return;
- }
-
- // HP — always available from CombatState.
- float hp = _vm.HealthPercent;
- renderer.Text("HP");
- renderer.SameLine();
- renderer.ProgressBar(hp, BarWidth, overlay: $"{hp * 100f:F0}%");
-
- // Stamina — show only when the VM has a real value.
- if (_vm.StaminaPercent is float stam)
- {
- renderer.Text("Stam");
- renderer.SameLine();
- renderer.ProgressBar(stam, BarWidth, overlay: $"{stam * 100f:F0}%");
- }
-
- // Mana — show only when the VM has a real value.
- if (_vm.ManaPercent is float mana)
- {
- renderer.Text("Mana");
- renderer.SameLine();
- renderer.ProgressBar(mana, BarWidth, overlay: $"{mana * 100f:F0}%");
- }
-
- renderer.End();
- }
-}
diff --git a/src/AcDream.UI.Abstractions/Panels/Vitals/VitalsVM.cs b/src/AcDream.UI.Abstractions/Panels/Vitals/VitalsVM.cs
deleted file mode 100644
index 6d512f29..00000000
--- a/src/AcDream.UI.Abstractions/Panels/Vitals/VitalsVM.cs
+++ /dev/null
@@ -1,75 +0,0 @@
-using AcDream.Core.Combat;
-
-namespace AcDream.UI.Abstractions.Panels.Vitals;
-
-///
-/// ViewModel for the vitals HUD panel. Reads live health percentage for the
-/// local player from (which is fed by the server's
-/// UpdateHealth (0x01C0) GameEvent).
-///
-///
-/// D.2a scope limits:
-///
-///
-///
-/// - HP comes from and is percent-only
-/// (0..1). Absolute current/max HP is not wired yet.
-/// - Stamina / Mana are always null — those values live in
-/// AppraiseInfoParser.CreatureProfile (parsed from
-/// PlayerDescription (0x0013)) but the parsed record is
-/// currently discarded. Wiring a LocalPlayerState cache is
-/// a separate follow-up; see docs/ISSUES.md.
-///
-///
-///
-/// GUID timing: the local player's server GUID isn't known at
-/// OnLoad (pre-login). Construct with
-/// left as 0; GameWindow calls the setter when the live session
-/// receives its guid at EnterWorld. Before the GUID is set,
-/// returns 1.0 (via CombatState's safe
-/// default for unknown guids) — the bar reads "full", which is harmless.
-///
-///
-public sealed class VitalsVM
-{
- private readonly CombatState _combat;
- private uint _localPlayerGuid;
-
- ///
- /// Build a VitalsVM bound to a instance. The
- /// GUID starts at 0; call once the
- /// live session assigns it.
- ///
- public VitalsVM(CombatState combat)
- {
- _combat = combat ?? throw new ArgumentNullException(nameof(combat));
- _localPlayerGuid = 0;
- }
-
- ///
- /// Push the authoritative local-player GUID from WorldSession.
- /// One-way setter — only GameWindow should call it, exactly once
- /// per live session.
- ///
- public void SetLocalPlayerGuid(uint guid) => _localPlayerGuid = guid;
-
- ///
- /// Current health percent (0..1) for the local player. Returns 1.0
- /// before login or if the server has never sent an UpdateHealth for
- /// this GUID.
- ///
- public float HealthPercent => _combat.GetHealthPercent(_localPlayerGuid);
-
- ///
- /// Stamina percent (0..1) or null when absolute values aren't wired.
- /// D.2a always returns null; to be populated by a future
- /// LocalPlayerState that caches PlayerDescription (0x0013).
- ///
- public float? StaminaPercent => null;
-
- ///
- /// Mana percent (0..1) or null when absolute values aren't wired.
- /// Same status as .
- ///
- public float? ManaPercent => null;
-}
diff --git a/src/AcDream.UI.ImGui/AcDream.UI.ImGui.csproj b/src/AcDream.UI.ImGui/AcDream.UI.ImGui.csproj
deleted file mode 100644
index 66f5bbad..00000000
--- a/src/AcDream.UI.ImGui/AcDream.UI.ImGui.csproj
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
- net10.0
- enable
- enable
- latest
- true
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/AcDream.UI.ImGui/ImGuiBootstrapper.cs b/src/AcDream.UI.ImGui/ImGuiBootstrapper.cs
deleted file mode 100644
index bbcc3a81..00000000
--- a/src/AcDream.UI.ImGui/ImGuiBootstrapper.cs
+++ /dev/null
@@ -1,64 +0,0 @@
-using Silk.NET.Input;
-using Silk.NET.OpenGL;
-using Silk.NET.OpenGL.Extensions.ImGui;
-using Silk.NET.Windowing;
-
-namespace AcDream.UI.ImGui;
-
-///
-/// Owns the ImGuiController from Silk.NET.OpenGL.Extensions.ImGui,
-/// which handles the whole Silk.NET ↔ ImGui.NET integration:
-///
-/// - Creates the ImGui context + OpenGL3 backend using Silk.NET's GL binding
-/// (no GLFW / SDL dependency — unlike Hexa.NET.ImGui, which assumed one).
-/// - Subscribes to Silk.NET's window + input events to drive IO.
-/// - Per frame: Update(dt) calls ImGui.NewFrame(); Render()
-/// calls ImGui.Render() + uploads draw data via its OpenGL3 backend.
-///
-///
-///
-/// Instance-scoped rather than static so GL-context lifetime is explicit.
-/// GameWindow owns the one instance and disposes on shutdown.
-///
-///
-///
-/// History: tried Hexa.NET.ImGui + Hexa.NET.ImGui.Backends.OpenGL3 first
-/// per the original plan, but its native OpenGL3 backend resolves GL functions
-/// via GLFW / SDL internally and crashed (0xC0000005) in InitNative without
-/// one of those present. Pivoted to the official Silk.NET extension on 2026-04-25.
-///
-///
-public sealed class ImGuiBootstrapper : IDisposable
-{
- private readonly ImGuiController _controller;
-
- public ImGuiBootstrapper(GL gl, IView window, IInputContext input)
- {
- ArgumentNullException.ThrowIfNull(gl);
- ArgumentNullException.ThrowIfNull(window);
- ArgumentNullException.ThrowIfNull(input);
- // ImGuiController constructor handles:
- // - ImGui.CreateContext()
- // - ImGuiOpenGL3 shader + vertex-buffer init (via Silk.NET GL)
- // - Keyboard + mouse event subscription (bound to Silk.NET IInputContext)
- // - Default style = dark
- _controller = new ImGuiController(gl, window, input);
- }
-
- ///
- /// Begin an ImGui frame. Call BEFORE any ImGui.* widget calls.
- /// Internally: consumes buffered input events, calls ImGui.NewFrame().
- ///
- public void BeginFrame(float deltaSeconds) => _controller.Update(deltaSeconds);
-
- ///
- /// Finalise the ImGui frame and draw to the framebuffer. Call AFTER all
- /// panel draws, within the same frame as . The
- /// OpenGL3 backend save/restores the GL state it touches (shader, VAO,
- /// texture, blend, scissor); state not in its save-list (e.g.
- /// GL_FRAMEBUFFER_SRGB) is caller's responsibility.
- ///
- public void Render() => _controller.Render();
-
- public void Dispose() => _controller.Dispose();
-}
diff --git a/src/AcDream.UI.ImGui/ImGuiPanelHost.cs b/src/AcDream.UI.ImGui/ImGuiPanelHost.cs
deleted file mode 100644
index d9a3a4c1..00000000
--- a/src/AcDream.UI.ImGui/ImGuiPanelHost.cs
+++ /dev/null
@@ -1,46 +0,0 @@
-using AcDream.UI.Abstractions;
-
-namespace AcDream.UI.ImGui;
-
-///
-/// implementation for the ImGui backend. Owns the
-/// registered panel set; iterates + draws every frame when the caller is
-/// inside an ImGui frame (between ImGui.NewFrame and
-/// ImGui.Render).
-///
-///
-/// This class does not call ImGui.NewFrame / ImGui.Render
-/// itself. Those belong to the caller (GameWindow) so GL-state
-/// ownership is explicit and the render-loop integration point is obvious.
-///
-///
-public sealed class ImGuiPanelHost : IPanelHost
-{
- private readonly Dictionary _panels = new();
- private readonly ImGuiPanelRenderer _renderer = new();
-
- ///
- public void Register(IPanel panel)
- {
- ArgumentNullException.ThrowIfNull(panel);
- _panels[panel.Id] = panel; // idempotent by Id
- }
-
- ///
- public void Unregister(string panelId) => _panels.Remove(panelId);
-
- ///
- public void RenderAll(PanelContext ctx)
- {
- // Order-independent — ImGui windows stack in the order they're drawn
- // for focus purposes but we have <=1 panel in D.2a.
- foreach (var panel in _panels.Values)
- {
- if (!panel.IsVisible) continue;
- panel.Render(ctx, _renderer);
- }
- }
-
- /// Current registered count (for diagnostics).
- public int Count => _panels.Count;
-}
diff --git a/src/AcDream.UI.ImGui/ImGuiPanelRenderer.cs b/src/AcDream.UI.ImGui/ImGuiPanelRenderer.cs
deleted file mode 100644
index d1fde2f9..00000000
--- a/src/AcDream.UI.ImGui/ImGuiPanelRenderer.cs
+++ /dev/null
@@ -1,42 +0,0 @@
-using System.Numerics;
-using AcDream.UI.Abstractions;
-using ImGuiNET;
-
-namespace AcDream.UI.ImGui;
-
-///
-/// implemented as thin wrappers around
-/// ImGui.NET calls. This is the ONLY place where ImGuiNET types appear
-/// outside of bootstrap plumbing — panels that need a feature must
-/// extend the abstraction here, not by importing ImGuiNET in panel
-/// files.
-///
-public sealed class ImGuiPanelRenderer : IPanelRenderer
-{
- ///
- public bool Begin(string title) => ImGuiNET.ImGui.Begin(title);
-
- ///
- public void End() => ImGuiNET.ImGui.End();
-
- ///
- public void Text(string text) => ImGuiNET.ImGui.TextUnformatted(text);
-
- ///
- public void SameLine() => ImGuiNET.ImGui.SameLine();
-
- ///
- public void Separator() => ImGuiNET.ImGui.Separator();
-
- ///
- public void ProgressBar(float fraction, float width, string? overlay = null)
- {
- // Clamp defensively; ImGui clamps internally but the abstraction
- // contract promises to handle out-of-range values.
- if (fraction < 0f) fraction = 0f;
- else if (fraction > 1f) fraction = 1f;
-
- var size = new Vector2(width, 0f); // height 0 → ImGui picks based on font
- ImGuiNET.ImGui.ProgressBar(fraction, size, overlay ?? string.Empty);
- }
-}
diff --git a/tests/AcDream.UI.Abstractions.Tests/AcDream.UI.Abstractions.Tests.csproj b/tests/AcDream.UI.Abstractions.Tests/AcDream.UI.Abstractions.Tests.csproj
deleted file mode 100644
index f99c6e22..00000000
--- a/tests/AcDream.UI.Abstractions.Tests/AcDream.UI.Abstractions.Tests.csproj
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
- net10.0
- enable
- enable
- false
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/tests/AcDream.UI.Abstractions.Tests/NullCommandBusTests.cs b/tests/AcDream.UI.Abstractions.Tests/NullCommandBusTests.cs
deleted file mode 100644
index 6f34d5ea..00000000
--- a/tests/AcDream.UI.Abstractions.Tests/NullCommandBusTests.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-namespace AcDream.UI.Abstractions.Tests;
-
-public sealed class NullCommandBusTests
-{
- private sealed record FakeCmd(int Value);
-
- [Fact]
- public void Publish_DoesNotThrow_OnAnyRecordType()
- {
- var bus = NullCommandBus.Instance;
-
- bus.Publish(new FakeCmd(42));
- bus.Publish("a string command");
- bus.Publish(12345);
- }
-
- [Fact]
- public void Instance_IsSingleton()
- {
- Assert.Same(NullCommandBus.Instance, NullCommandBus.Instance);
- }
-}
diff --git a/tests/AcDream.UI.Abstractions.Tests/PanelContextTests.cs b/tests/AcDream.UI.Abstractions.Tests/PanelContextTests.cs
deleted file mode 100644
index 2a665c93..00000000
--- a/tests/AcDream.UI.Abstractions.Tests/PanelContextTests.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-namespace AcDream.UI.Abstractions.Tests;
-
-public sealed class PanelContextTests
-{
- [Fact]
- public void Fields_RoundTripThroughConstructor()
- {
- var ctx = new PanelContext(DeltaSeconds: 0.016f, Commands: NullCommandBus.Instance);
-
- Assert.Equal(0.016f, ctx.DeltaSeconds);
- Assert.Same(NullCommandBus.Instance, ctx.Commands);
- }
-
- [Fact]
- public void RecordEquality_ByValue()
- {
- var a = new PanelContext(1f / 60f, NullCommandBus.Instance);
- var b = new PanelContext(1f / 60f, NullCommandBus.Instance);
-
- // Record-struct equality is value-based on DeltaSeconds + reference-based
- // on Commands (since ICommandBus is a reference type, same instance → equal).
- Assert.Equal(a, b);
- }
-}
diff --git a/tests/AcDream.UI.Abstractions.Tests/VitalsVMTests.cs b/tests/AcDream.UI.Abstractions.Tests/VitalsVMTests.cs
deleted file mode 100644
index 3abf2c35..00000000
--- a/tests/AcDream.UI.Abstractions.Tests/VitalsVMTests.cs
+++ /dev/null
@@ -1,80 +0,0 @@
-using AcDream.Core.Combat;
-using AcDream.UI.Abstractions.Panels.Vitals;
-
-namespace AcDream.UI.Abstractions.Tests;
-
-public sealed class VitalsVMTests
-{
- [Fact]
- public void HealthPercent_ReturnsCombatStateValue_AfterUpdateHealth()
- {
- var combat = new CombatState();
- uint guid = 0x5000_0042u;
- combat.OnUpdateHealth(guid, 0.42f);
-
- var vm = new VitalsVM(combat);
- vm.SetLocalPlayerGuid(guid);
-
- Assert.Equal(0.42f, vm.HealthPercent, precision: 3);
- }
-
- [Fact]
- public void HealthPercent_ReturnsOne_WhenGuidUnknown()
- {
- var combat = new CombatState();
- var vm = new VitalsVM(combat);
-
- // No SetLocalPlayerGuid call — defaults to 0 which CombatState has never seen.
- Assert.Equal(1f, vm.HealthPercent);
- }
-
- [Fact]
- public void HealthPercent_ReturnsOne_WhenGuidSetButNeverUpdated()
- {
- var combat = new CombatState();
- var vm = new VitalsVM(combat);
- vm.SetLocalPlayerGuid(0xDEAD_BEEFu);
-
- Assert.Equal(1f, vm.HealthPercent);
- }
-
- [Fact]
- public void StaminaPercent_IsNull_ForD2aScope()
- {
- // D.2a explicitly defers Stamina until LocalPlayerState + PlayerDescription
- // wiring. When that arrives VitalsVM.StaminaPercent becomes non-null and
- // VitalsPanel starts drawing the Stam bar automatically.
- var vm = new VitalsVM(new CombatState());
- Assert.Null(vm.StaminaPercent);
- }
-
- [Fact]
- public void ManaPercent_IsNull_ForD2aScope()
- {
- var vm = new VitalsVM(new CombatState());
- Assert.Null(vm.ManaPercent);
- }
-
- [Fact]
- public void SetLocalPlayerGuid_ReroutesHealthLookup_WithoutStaleCache()
- {
- // Simulate the realistic GameWindow flow: VM is constructed pre-login
- // with GUID=0, then SetLocalPlayerGuid is called at EnterWorld.
- var combat = new CombatState();
- uint playerGuid = 0x5003_E219u;
- combat.OnUpdateHealth(playerGuid, 0.75f);
-
- var vm = new VitalsVM(combat);
- // Before SetLocalPlayerGuid — reads GUID=0 → returns safe 1.0.
- Assert.Equal(1f, vm.HealthPercent);
-
- vm.SetLocalPlayerGuid(playerGuid);
- Assert.Equal(0.75f, vm.HealthPercent, precision: 3);
- }
-
- [Fact]
- public void Constructor_ThrowsOnNullCombat()
- {
- Assert.Throws(() => new VitalsVM(null!));
- }
-}