# Phase B.7 — Vivid Target Indicator — design **Date:** 2026-05-15. **Status:** DESIGN — implementation starting same session. **Closes (partially):** [issue #59](../../ISSUES.md) (`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 selectedGuidProvider` - `Func 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.