# 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<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.
