feat(D.5.3a): selected-object meter — Health bar + name on the action bar

Port of gmToolbarUI::HandleSelectionChanged (acclient_2013_pseudo_c.txt:198635).
When the player selects a world object the action bar's bottom strip shows the
object name + (for player/pet/attackable targets) a live Health meter; deselect
clears it. Mana (#140) + stack slider deferred.

- SelectedObjectController (new): clear-then-populate on selection change; sets
  name (UiText child, VitalsController pattern), overlay state (ObjectSelected /
  StackedItemSelected via UiDatElement.ActiveState), shows the health meter and
  sends QueryHealth for health targets. Subscribes via a delegate seam (no
  GameWindow coupling).
- GameWindow: _selectedGuid field -> SelectedGuid property + SelectionChanged
  event (fires on actual change only); 3 write sites converted, reads untouched.
  All selection-write paths (LMB pick, Tab/Q, despawn-clear via Tick()) run on
  the render thread, so the event-driven UI mutation is single-threaded.
- WorldSession.SendQueryHealth (0x01BF) — wraps SocialActions.BuildQueryHealth.
- DatWidgetFactory.BuildMeter: handle the single-image toolbar meter shape
  (back-track on the element's own DirectState, fill on one Type-3 child). The
  sprites go in the TILE slot (DrawMode=Normal tiles to full bar geometry per
  UIElement_Meter::DrawChildren) — a left-cap assignment would gap/clamp a
  sub-140px sprite. Vitals 3-slice path unchanged.
- ToolbarController.HiddenIds: A1 (health) now owned by SelectedObjectController;
  A2 (mana) + A4 (stack) stay hidden (deferred) so their dat back-tracks don't
  render as stray empty bars.

Adversarial Opus review found + fixed: the mana-meter orphan (A2 left unhidden)
and the meter tile-vs-cap render bug (C1). Divergence rows AP-46 (health gate
approximation: IsLiveCreatureTarget vs IsPlayer||pet||attackable) + AP-47
(meter shown on select vs on UpdateHealth reply). Spec §5 corrected.

Build + full test suite green (2,684 passed / 4 skipped). Health meter render
fidelity (full-width fill + fraction mapping) pending the user's visual gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-18 22:47:24 +02:00
parent e8562fc4e2
commit 6636e50c2a
11 changed files with 851 additions and 30 deletions

View file

@ -48,7 +48,7 @@ Copy this block when adding a new issue:
## #140 — Toolbar interactivity — selected-object display
**Status:** OPEN
**Status:** IN PROGRESS (D.5.3a — health + name landed, pending visual gate; mana + stack slider still deferred)
**Severity:** MEDIUM
**Filed:** 2026-06-17
**Component:** ui — D.5 toolbar / selection
@ -56,6 +56,7 @@ Copy this block when adding a new issue:
**Description:** The action bar (D.5.1) is the retail "selected object" display. Wire the B.4 WorldPicker/selection state to the toolbar's currently-hidden elements: the two meters 0x100001A1 (selected-object Health) / 0x100001A2 (selected-object Mana) + the stack slider 0x100001A4 + the object-name line, so the bar shows what the player has selected in the world. Click-to-use + the peace/war stance indicator already shipped in D.5.1. Promote to roadmap D.5.3 (already listed there).
**Root cause / status:** The selection-state wire was deferred out of D.5.1 scope; the meter/slider elements are present in LayoutDesc 0x21000016 but hidden (no backing data). D.5.3 is the planned port.
- **D.5.3a (2026-06-18):** the Health meter (0x100001A1) + the object-name line (0x1000019F) + the overlay state (0x100001A0) are wired via `SelectedObjectController` (port of `gmToolbarUI::HandleSelectionChanged`); `SelectionChanged` event on `GameWindow`; `QueryHealth (0x01BF)` sent on select. Spec/plan: `docs/superpowers/specs|plans/2026-06-18-d53a-*`. **Still deferred:** the Mana meter (0x100001A2 — owned-item-only; no remote-target mana path yet) and the stack entry/slider (0x100001A3/A4 — stack-split UI). Divergence rows AP-46/AP-47. Awaiting the visual gate before closing the health half.
**Files:** `src/AcDream.App/UI/Layout/ToolbarController.cs` + the selection/WorldPicker state (see `claude-memory/project_interaction_pipeline.md`).

View file

@ -144,6 +144,8 @@ accepted-divergence entries (#96, #49, #50).
| AP-41 | Scrollbar thumb 3-slice cap fallback only: single-tile draw (`0x06004C63`) used only when `ThumbTopSprite`/`ThumbBotSprite` are unset; the chat controller passes all three cap ids so the 3-slice path is drawn in practice | `src/AcDream.App/UI/UiScrollbar.cs:35` | The fallback single-tile path is unreachable when caps are bound (chat controller always sets them); the 3-slice path is the active code path | Only if a future caller omits the cap ids will the fallback fire — no visual regression in the chat window | `UIElement_Scrollbar::UpdateLayout @0x4710d0`; cap sprites `0x06004C60` (top) + `0x06004C66` (bottom) from base layout `0x2100003E` |
| AP-42 | `UiMenu` item model is flat (label + opaque payload, single-level popup); retail `UIElement_Menu::MakePopup @0x46d310` supports hierarchical nested submenus via recursive popup chain | `src/AcDream.App/UI/UiMenu.cs` | The chat talk-focus menu is single-level (14 rows, 2 columns, no submenu); hierarchy is latent and unreachable through the chat window — no behavioral difference in the current usage | A future menu with nested submenus would render flat (only the top-level items drawn, no drill-down) | `UIElement_Menu::MakePopup` @0x46d310 |
| AP-45 | `PublicUpdatePropertyInt (0x02CE)` sequence byte parsed-past but not honored; last update wins (no freshness check against sequence number) | `src/AcDream.Core.Net/Messages/PublicUpdatePropertyInt.cs` | Loopback ACE rarely reorders; latest-wins matches `PrivateUpdateVital`/`UpdatePosition`'s existing non-sequence behavior. Sequence tracking added when needed alongside TS-26. | A reordered 0x02CE on a real network could apply a stale UiEffects value — item icon temporarily shows the wrong effect state, corrected on next update | `PublicUpdatePropertyInt` sequence byte (ACE GameMessagePublicUpdatePropertyInt) |
| AP-46 | Health-meter gate approximation: retail shows the health meter for `IsPlayer() || pet_owner || ObjectIsAttackable()`; acdream uses `IsLiveCreatureTarget` (the `ItemType.Creature` flag) | `src/AcDream.App/UI/Layout/SelectedObjectController.cs` (`HandleSelectionChanged` analogue) | `IsLiveCreatureTarget` is already wired for Tab/Q combat-target gating and is the correct proxy for M1.5 scope (no pet system, no PK); the only practical gap is a friendly non-attackable NPC, which is rare in the ACE dev loop | A friendly (non-attackable) NPC shows a health meter where retail would show name+overlay only — false meter on non-combat NPCs | `gmToolbarUI::HandleSelectionChanged` acclient_2013_pseudo_c.txt:198754 |
| AP-47 | Meter-visible timing: acdream shows the health meter immediately on select; retail shows it from `RecvNotice_UpdateObjectHealth` when the queried value arrives | `src/AcDream.App/UI/Layout/SelectedObjectController.cs` (`OnSelectionChanged`) | Avoids a one-round-trip blank-then-pop; the fill polls `GetHealthPercent` which returns 1.0 until the reply — visually indistinguishable for a full-HP target and self-corrects within one RTT for a damaged target | A freshly-selected off-screen-damaged target reads full for one server round-trip before the `QueryHealth` reply lands | `gmToolbarUI::HandleSelectionChanged` acclient_2013_pseudo_c.txt:198757 |
---

View file

@ -223,9 +223,12 @@ AcDream.App.UI.Layout.SelectedObjectController.Bind(
datFont: vitalsDatFont);
```
Also: remove `0x100001A1` and `0x100001A2` from `ToolbarController.HiddenIds` (single-owner: the
selected-object meters are now owned by `SelectedObjectController`); `0x100001A4` (stack slider) stays
in `HiddenIds` (deferred). Convert the `_selectedGuid` field → the `SelectedGuid` property (unit 1).
Also: remove **only** `0x100001A1` from `ToolbarController.HiddenIds` — the health meter is now owned
by `SelectedObjectController` (it hides A1 at bind, shows on a health-target select). `0x100001A2`
(mana, deferred #140) and `0x100001A4` (stack slider, deferred) **stay** in `HiddenIds`: they have no
controller yet, so they must stay hidden or their dat back-track sprites render as stray empty bars.
(`HiddenIds = { 0x100001A2, 0x100001A4 }`.) Convert the `_selectedGuid` field → the `SelectedGuid`
property (unit 1).
## Data flow