acdream/docs/superpowers/specs/2026-05-15-phase-b7-target-indicator-design.md
Erik 37177a418e docs(B.7): design spec for Vivid Target Indicator (selection feedback)
Retail-anchored design for the missing visual feedback on selection:
four corner triangles + radar-blip colour coding around the selected
entity, drawn via ImGui in screen space.

Retail evidence (named decomp):
  * VividTargetIndicator::SetSelected at 0x004f5ce0
  * gmRadarUI::GetBlipColor at 0x004d76f0 (Portal / Vendor / Creature /
    Player / PK / PKLite / Default colours from pwd._bitfield bits +
    IsCreature/IsPlayer/IsPK predicates we already parse)
  * VividTargetIndicator::CopyImage at 0x004f5dd0 (tints a source
    bitmap by RGBA)

MVP scope:
  1. RadarBlipColors helper (Core, with unit tests)
  2. TargetIndicatorPanel (App, ImGui draw via background draw list)
  3. Wire to existing _selectedGuid from B.4b
  4. ~200 LOC + tests

Deferred to follow-ups: off-screen edge arrow, DAT-loaded sprite (MVP
draws procedurally), mesh-tint highlight, player-option toggle, server
selection-relay.

Pairs with #59 (WorldPicker over-pick): the indicator makes the
mis-pick visible, so the user can clear + reselect even before the
underlying picker is tightened.
2026-05-15 06:46:55 +02:00

8.1 KiB
Raw Blame History

Phase B.7 — Vivid Target Indicator — design

Date: 2026-05-15. Status: DESIGN — implementation starting same session. Closes (partially): issue #59 (WorldPicker over-pick — by making the wrong pick visible so the user can correct before clicking). Predecessors: B.4b (selection), B.5 (pickup), B.6 (auto-walk). Retail anchors (decomp docs/research/named-retail/acclient_2013_pseudo_c.txt):

  • VividTargetIndicator::SetSelected at 0x004f5ce0 — selection setter + filtering.
  • gmRadarUI::GetBlipColor(weenie) at 0x004d76f0 — per-type colour table.
  • VividTargetIndicator::CopyImage at 0x004f5dd0 — tints a source bitmap by RGBA, four blit methods supported.
  • VividTargetIndicator::UpdateDisplayState at 0x004f5fb0 — gated on PlayerOption_VividTargetingIndicator.
  • Two render surfaces: m_pOnScreen (corners around target) + m_pOffScreen (edge arrow when target is off-camera).

Problem

User sees Tirenia chat dialogue after intending to click an item. Cause: WorldPicker.Pick's 5 m fixed radius over-picks bigger collision spheres (NPCs > items). Without on-screen feedback, the user can't tell their intended click missed — they only see the consequence (NPC walks toward, dialogue fires).

Retail solved this with selection feedback drawn over the 3D scene: four small corner triangles framing the target's silhouette, colour-coded by entity type (yellow ≈ creature, white ≈ default, red ≈ PK, etc.). The mere existence of the indicator makes the picker bug self-correcting — you click empty space to deselect, then click again on the actual target.

This is the missing UX feedback that's been silently breaking interaction since B.4b shipped.


Scope decisions

Included in B.7 MVP:

  1. Per-entity colour lookup, port of gmRadarUI::GetBlipColor using our existing ItemType and ObjectDescriptionFlags (already parsed from CreateObject).
  2. Screen-space corner-triangle renderer drawn via ImGui's ImDrawList (we already have ImGui — no new GL infrastructure).
  3. Hook to _selectedGuid — updates whenever B.4b's PickAndStoreSelection mutates the selection.
  4. Hidden when no selection, or when the selected entity is no longer in _entitiesByServerGuid (despawned).

Deferred to B.7 follow-ups:

  • Off-screen edge arrow (m_pOffScreen). Useful for tracking a target you walked past; not MVP-critical.
  • Retail-faithful corner-triangle imagery loaded from the DAT. MVP draws procedurally-coloured equilateral triangles via ImGui — looks acceptable, doesn't need a DAT-asset hunt.
  • Mesh-tint highlight ("texture lights up a bit"). That's a shader-level change on the selected entity's mesh. Requires touching WbDrawDispatcher to flag a per-instance highlight uniform. Two-line stub already mentioned in the mesh_modern.vert InstanceData struct docstring (per CLAUDE.md WB integration cribs) — pick it up if the corner triangles aren't enough.
  • Player-option toggle (PlayerOption_VividTargetingIndicator). MVP is always-on; add toggle if users complain about screenshots.
  • Server selection-relay (SmartBox::SetTargetObjectID outbound, RecvNotice_ServerSaysMoveItem inbound). Not visible in current ACE behaviour and not blocking M1.

Architecture

Single new file: src/AcDream.App/UI/TargetIndicatorPanel.cs (or similar — fits the existing AcDream.UI.Abstractions.Panels.* pattern but lives in AcDream.App because it touches GameWindow's camera projection state).

Responsibilities:

  • Read _selectedGuid (passed in via constructor delegate, like the existing DebugVM wiring).
  • Look up the entity in _entitiesByServerGuid + _liveEntityInfoByGuid.
  • Compute world-space AABB centre + radius (use cached EntitySpawn.ItemType for type bits + a fixed visual radius — 1 m default, or per-itemType later).
  • Project the AABB to 4 screen-space corner positions using the active camera's View × Projection × Viewport.
  • Resolve colour via RadarBlipColors.For(itemType, objectDescriptionFlags) — new static helper class.
  • Draw 4 small filled triangles via ImGui.GetBackgroundDrawList().AddTriangleFilled.

Render order: background-draw-list, AFTER the 3D scene, BEFORE other ImGui panels so other UI can occlude the triangles if needed.


Colour table

Per gmRadarUI::GetBlipColor decomp lines 219913+, the dispatch order is:

if pwd._bitfield & 0x40000              → Portal      (cyan-ish?)
if pwd._bitfield[1] & 0x02              → Vendor      (green?)
if (pwd._bitfield & 0x10) && IsCreature && !IsPlayer → Creature (yellow)
if IsPlayer:
    if  IsPK                            → PlayerKiller (red)
    elif IsPKLite                       → PKLite      (pink?)
    elif pwd._bitfield & 0x200000       → Creature-coloured (hostile player flag?)
    else                                → Default     (white)
…  (more branches above the read window)

I'll port the dispatch verbatim. The actual RGBAColor_Radar* constants live in retail data (probably in acclient.h per CLAUDE.md's named-retail anchors). For MVP I'll use hand-picked colours that visually match retail screenshots; refine later if I find the constants.


Triangle geometry

Each corner triangle in retail is a small right-angle triangle pointing into the centre of the selection rectangle — i.e., the top-left corner has its hypotenuse along the screen-up-then-screen-right diagonals, pointing down-right toward the entity. Same pattern at the other three corners (rotated 90° / 180° / 270°).

MVP: small filled triangles (~8 px legs) at each corner of the projected AABB, oriented inward. Fine-tune via screenshots vs retail later.


File-level scope sketch

  • New: src/AcDream.Core/Ui/RadarBlipColors.cs (AcDream.Core so it's testable independently of App). Static class with RGBA For(ItemType, ObjectDescriptionFlags) returning a tagged colour. ~60 LOC.
  • New: src/AcDream.App/UI/TargetIndicatorPanel.cs (~100 LOC). Owns the per-frame projection + ImGui draw. Constructor takes:
    • Func<uint?> selectedGuidProvider
    • Func<uint, EntitySpawn?> entityResolver (closure over _entitiesByServerGuid / _liveEntityInfoByGuid)
    • Func<(Matrix4x4 view, Matrix4x4 projection, Vector2 viewport)> cameraProvider
  • Modify: src/AcDream.App/Rendering/GameWindow.cs (~15 LOC). Construct the panel after ImGui init, wire delegates, call Render() from the ImGui pass.
  • Tests: tests/AcDream.Core.Tests/Ui/RadarBlipColorsTests.cs (~40 LOC, 5-6 cases covering the type-flag → colour matrix).

Total ~200 LOC + tests. Single phase, no slices needed.


Acceptance criteria

  • Single-click an NPC at Holtburg → yellow-ish corner triangles appear around it.
  • Single-click a ground item → white-ish triangles.
  • Click empty ground / deselect → triangles disappear.
  • Selected entity moves → triangles track it (project per frame).
  • Selected entity despawns → triangles disappear.
  • No measurable FPS regression at Holtburg radius=4 (it's 4 triangles per frame — should be invisible to perf).
  • Unit tests cover the colour-lookup matrix: NPC, item, player, PK, lifestone, portal, vendor.

Out of scope (deferred)

  • Off-screen edge arrow.
  • DAT-loaded triangle sprite (procedural for MVP).
  • Mesh-tint highlight on selected entity.
  • Player-option toggle.
  • Server selection-relay (SmartBox::SetTargetObjectID).
  • Tab-cycle selection (SelectClosestCombatTarget already exists for combat; non-combat cycle is a separate UX phase).

Out-of-band: addressing the picker over-pick (#59)

This phase deliberately does NOT fix the underlying picker. Once the indicator is shipped, the user can see the wrong selection and click empty space to clear it, then retry on the actual target. The picker fix (tighter per-itemType radius, ray-test against actual mesh bounds, or click priority by itemType) is a follow-up that can be informed by which combos most often produce mis-picks once we have the indicator showing them clearly.

If the indicator + picker need to be revisited together later, both end up in the same UX phase.