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:
parent
e8562fc4e2
commit
6636e50c2a
11 changed files with 851 additions and 30 deletions
|
|
@ -619,6 +619,8 @@ public sealed class GameWindow : IDisposable
|
|||
private AcDream.App.UI.UiHost? _uiHost;
|
||||
// Phase D.5.1 — toolbar controller (kept for lifetime clarity; mirrors _chatWindowController pattern).
|
||||
private AcDream.App.UI.Layout.ToolbarController? _toolbarController;
|
||||
// Phase D.5.3a — selected-object strip controller (name, overlay state, health meter).
|
||||
private AcDream.App.UI.Layout.SelectedObjectController? _selectedObjectController;
|
||||
// Phase D.2b Task 9 — plugin UI registrations buffered before OnLoad; drained in OnLoad.
|
||||
private readonly AcDream.App.Plugins.BufferedUiRegistry? _uiRegistry;
|
||||
// Phase I.2: ImGui debug panel ViewModel. Lives for as long as
|
||||
|
|
@ -846,6 +848,21 @@ public sealed class GameWindow : IDisposable
|
|||
private readonly Dictionary<uint, AcDream.Core.Net.WorldSession.EntitySpawn> _lastSpawnByGuid = new();
|
||||
// Current selection: written by Q-cycle (combat) and LMB click (interact); cleared on entity despawn.
|
||||
private uint? _selectedGuid;
|
||||
/// <summary>Fires when the selected world object changes (retail gmToolbarUI selection-change event,
|
||||
/// acclient_2013_pseudo_c.txt:198635). Private: only the internal SelectedObjectController subscribes.</summary>
|
||||
private event Action<uint?>? SelectionChanged;
|
||||
/// <summary>Currently-selected world object guid. The setter fires <see cref="SelectionChanged"/> only on
|
||||
/// an actual change (dedup), so all writes go through here; reads may use the field directly.</summary>
|
||||
private uint? SelectedGuid
|
||||
{
|
||||
get => _selectedGuid;
|
||||
set
|
||||
{
|
||||
if (_selectedGuid == value) return;
|
||||
_selectedGuid = value;
|
||||
SelectionChanged?.Invoke(value);
|
||||
}
|
||||
}
|
||||
|
||||
// B.6/B.7 (2026-05-16): pending close-range action that will be fired
|
||||
// once the local auto-walk overlay reports arrival (body has finished
|
||||
|
|
@ -2003,6 +2020,19 @@ public sealed class GameWindow : IDisposable
|
|||
warDigits: toolbarWarDigits,
|
||||
emptyDigits: toolbarEmptyDigits);
|
||||
|
||||
// Phase D.5.3a — selected-object strip (name, overlay state, health meter).
|
||||
// Analogue of retail gmToolbarUI::HandleSelectionChanged
|
||||
// (acclient_2013_pseudo_c.txt:198635).
|
||||
_selectedObjectController = AcDream.App.UI.Layout.SelectedObjectController.Bind(
|
||||
toolbarLayout,
|
||||
subscribeSelectionChanged: h => SelectionChanged += h,
|
||||
isHealthTarget: IsLiveCreatureTarget,
|
||||
name: g => Objects.Get(g)?.Name,
|
||||
healthPercent: g => Combat.GetHealthPercent(g),
|
||||
stackSize: g => (uint)(Objects.Get(g)?.StackSize ?? 0),
|
||||
sendQueryHealth: g => _liveSession?.SendQueryHealth(g),
|
||||
datFont: vitalsDatFont);
|
||||
|
||||
var toolbarRoot = toolbarLayout.Root;
|
||||
// Wrap the dat content in the universal 8-piece beveled window chrome —
|
||||
// the SAME UiNineSlicePanel used by the vitals and chat windows. The
|
||||
|
|
@ -3708,7 +3738,7 @@ public sealed class GameWindow : IDisposable
|
|||
_entitiesByServerGuid.Remove(serverGuid);
|
||||
_lastSpawnByGuid.Remove(serverGuid);
|
||||
if (_selectedGuid == serverGuid)
|
||||
_selectedGuid = null;
|
||||
SelectedGuid = null;
|
||||
|
||||
if (logDelete)
|
||||
_lightingSink?.UnregisterOwner(existingEntity.Id);
|
||||
|
|
@ -11568,7 +11598,7 @@ public sealed class GameWindow : IDisposable
|
|||
|
||||
if (picked is uint guid)
|
||||
{
|
||||
_selectedGuid = guid;
|
||||
SelectedGuid = guid;
|
||||
string label = DescribeLiveEntity(guid);
|
||||
Console.WriteLine($"[B.4b] pick guid=0x{guid:X8} name={label}");
|
||||
// B.7 (2026-05-15): one-shot per-pick diagnostic so we can
|
||||
|
|
@ -11958,7 +11988,7 @@ public sealed class GameWindow : IDisposable
|
|||
bestGuid = guid;
|
||||
}
|
||||
|
||||
_selectedGuid = bestGuid;
|
||||
SelectedGuid = bestGuid;
|
||||
if (bestGuid is { } selected)
|
||||
{
|
||||
string label = DescribeLiveEntity(selected);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue