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

128 lines
8.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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