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.
This commit is contained in:
Erik 2026-05-15 06:46:55 +02:00
parent 5612ce718a
commit 37177a418e

View file

@ -0,0 +1,128 @@
# 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.