feat(B.7): TargetIndicatorPanel — corner triangles around selected entity

Per the B.7 design spec, wires a Vivid-Target-Indicator-style overlay
into GameWindow's ImGui pass:

  TargetIndicatorPanel (src/AcDream.App/UI/TargetIndicatorPanel.cs)
    - Three delegates injected from GameWindow:
        selectedGuidProvider  -> _selectedGuid
        entityResolver        -> (worldPos, itemType, pwdBits) from
                                 _entitiesByServerGuid + _liveEntityInfoByGuid
                                 + _lastSpawnByGuid
        cameraProvider        -> (view, projection, viewport) from
                                 _cameraController.Active + _window.Size
    - Per-frame Render():
        * Bail on null selection / despawned entity / zero viewport.
        * Project entity world position (+0.9m mid-body offset) to NDC.
        * Bail off-screen (no edge arrow in MVP).
        * Convert to viewport pixel coords, draw 4 right-angle triangles
          at corners of a 48px square around the projected center.
        * Colour from RadarBlipColors.For(itemType, pwdBits).

  GameWindow wiring:
    - Construct _targetIndicator right after _panelHost during ImGui init.
    - Call _targetIndicator?.Render() between _panelHost.RenderAll and
      _imguiBootstrap.Render — draws to the ImGui background list so
      docked panels can occlude the indicator if they overlap.

Build green. Core.Tests went 1046 -> 1054 (+8 RadarBlipColors tests
from the prior commit). Baseline failures unchanged at 8.

Visual verification next: launch, click an NPC → yellow corners; click
an item -> white corners; deselect -> corners disappear.
This commit is contained in:
Erik 2026-05-15 06:54:24 +02:00
parent 8544a785d7
commit c7e5f9f00f
2 changed files with 187 additions and 0 deletions

View file

@ -565,6 +565,11 @@ public sealed class GameWindow : IDisposable
// 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;
// B.7 (2026-05-15): Vivid Target Indicator — four corner triangles
// around the selected entity, colour-coded by ItemType + PWD bits.
// Lives alongside the debug panels; cheap to construct + ignore
// when no selection. Spec: docs/superpowers/specs/2026-05-15-phase-b7-target-indicator-design.md
private AcDream.App.UI.TargetIndicatorPanel? _targetIndicator;
private AcDream.UI.Abstractions.Panels.Vitals.VitalsVM? _vitalsVm;
// Phase I.2: ImGui debug panel ViewModel. Lives for as long as
// _panelHost does. Self-subscribes to CombatState in its ctor, so
@ -1150,6 +1155,41 @@ public sealed class GameWindow : IDisposable
_imguiBootstrap = new AcDream.UI.ImGui.ImGuiBootstrapper(_gl!, _window!, _input!);
_panelHost = new AcDream.UI.ImGui.ImGuiPanelHost();
// B.7 Vivid Target Indicator — corner-triangle highlight
// around the currently-selected entity. Delegates pull
// live state from this GameWindow instance every frame:
// - selected guid → _selectedGuid (set by PickAndStoreSelection)
// - entity resolver → position from _entitiesByServerGuid +
// itemType / PWD bits from cached LiveEntityInfo + last spawn
// - camera → _cameraController.Active or (zero) when not
// yet ready, in which case the panel bails on viewport==0.
_targetIndicator = new AcDream.App.UI.TargetIndicatorPanel(
selectedGuidProvider: () => _selectedGuid,
entityResolver: guid =>
{
if (!_entitiesByServerGuid.TryGetValue(guid, out var entity))
return null;
uint rawItemType = 0;
if (_liveEntityInfoByGuid.TryGetValue(guid, out var info))
rawItemType = (uint)info.ItemType;
uint pwdBits = 0;
if (_lastSpawnByGuid.TryGetValue(guid, out var spawn)
&& spawn.ObjectDescriptionFlags is { } odf)
pwdBits = odf;
return new AcDream.App.UI.TargetIndicatorPanel.TargetInfo(
entity.Position, rawItemType, pwdBits);
},
cameraProvider: () =>
{
if (_cameraController is null || _window is null)
return (System.Numerics.Matrix4x4.Identity,
System.Numerics.Matrix4x4.Identity,
System.Numerics.Vector2.Zero);
var cam = _cameraController.Active;
return (cam.View, cam.Projection,
new System.Numerics.Vector2(_window.Size.X, _window.Size.Y));
});
// 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. Stam/Mana
@ -7043,6 +7083,11 @@ public sealed class GameWindow : IDisposable
}
_panelHost.RenderAll(ctx);
// B.7 Vivid Target Indicator: draws corner triangles to the
// ImGui background draw list so it appears behind any docked
// panels but still over the 3D scene. Cheap when no
// selection — internal early-return on null guid.
_targetIndicator?.Render();
_imguiBootstrap.Render();
}