using System;
using System.Numerics;
using AcDream.Core.Ui;
using ImGuiNET;
namespace AcDream.App.UI;
///
/// B.7 (2026-05-15) — Vivid Target Indicator. Draws four small
/// corner triangles around the currently-selected entity, colour-coded
/// by entity type (NPCs yellow, items white-ish, PKs red, etc.).
/// Retail-faithful equivalent of VividTargetIndicator
/// (named decomp at 0x004d6165 / 0x004f5ce0).
///
///
/// MVP scope: on-screen indicator only, drawn via ImGui's background
/// draw list. Deferred to follow-ups: off-screen edge arrow, DAT-loaded
/// triangle sprite, mesh-tint highlight, player-option toggle.
///
///
///
/// The panel pulls its inputs through delegates supplied by the host
/// () so it doesn't have to depend
/// on internal state types:
///
///
/// - selectedGuidProvider — host's current _selectedGuid.
/// - entityResolver — returns
/// for a given guid, or null if
/// the entity is no longer in the world (despawned).
/// - cameraProvider — host's active camera + viewport
/// dimensions; called once per frame.
///
///
public sealed class TargetIndicatorPanel
{
///
/// What the panel needs to know about the selected entity per frame.
/// ItemType + ObjectDescriptionFlags feed
/// for colour selection.
/// Scale multiplies the per-type base height in
/// — a scaled-up sign or oversized NPC
/// gets a proportionally bigger box. Useability (acclient.h:6478
/// ITEM_USEABLE enum) discriminates real pickup items
/// (USEABLE_REMOTE bit set, 0.8 m boxes) from same-ItemType-but-non-
/// useable scenery like signs (USEABLE_UNDEF, 3 m boxes).
///
public readonly record struct TargetInfo(
Vector3 WorldPosition,
uint ItemType,
uint ObjectDescriptionFlags,
float Scale,
uint? Useability = null,
// 2026-05-16: world-space SelectionSphere center + radius.
// Comes from the Setup's baked selection_sphere (acclient.h
// CSetup::selection_sphere) scaled by entity scale. When
// populated, the panel projects the sphere as a screen circle
// and uses that as the indicator rect — matches retail
// SmartBox::GetObjectBoundingBox (decomp 0x00452e20). When
// null, the panel falls back to the per-type height heuristic.
Vector3? WorldSphereCenter = null,
float? WorldSphereRadius = null);
private readonly Func _selectedGuidProvider;
private readonly Func _entityResolver;
private readonly Func<(Matrix4x4 View, Matrix4x4 Projection, Vector2 Viewport)> _cameraProvider;
///
/// Pixel size of each corner triangle's right-angle legs.
/// Retail uses UIRegion::GetWidth(m_rgOnScreenCorners.m_data[1])
/// of the triangle sprite (decomp 0x004f69c8). The retail
/// sprite is small — ~8 px legs. 14 was too chunky per user
/// feedback on 2026-05-16; 8 matches the retail screenshot.
///
public float TriangleSize { get; set; } = 8f;
///
/// World-space height of the indicator box for entities that don't
/// have a more specific type tag. Items use a smaller value (see
/// ). 1.8 m matches a standing humanoid;
/// short items still get a small box because the projection
/// preserves apparent size.
///
public float EntityHeight { get; set; } = 1.8f;
///
/// Defensive fallback height when the entity has no usable
/// SelectionSphere (Radius ≤ 1e-4f). With B.7's sphere-projection
/// path active (since commit f4f4143), this fallback only fires
/// for entities whose Setup didn't bake a selection sphere —
/// rare in practice. The single 1.5 m × scale default is a sane
/// midpoint; per-type branches were retired in the 2026-05-16
/// Commit B because the sphere path is authoritative.
///
public float EntityHeightFor(uint itemType, uint pwdBitfield, float scale, uint? useability = null)
{
if (scale <= 0f) scale = 1f;
return 1.5f * scale;
}
///
/// Box width = projected height ×
/// . Retail's Vivid Target Indicator
/// draws a square box — four corner triangles arranged in a square —
/// so 1.0 = width matches height. The earlier 0.5 (humanoid-ish
/// aspect) made the box uncomfortably narrow for non-humanoids.
///
public float WidthHeightRatio { get; set; } = 1.0f;
///
/// Floor for the projected screen height (pixels). Prevents the
/// indicator from collapsing to a point on far-away entities.
///
public float MinScreenHeight { get; set; } = 16f;
public TargetIndicatorPanel(
Func selectedGuidProvider,
Func entityResolver,
Func<(Matrix4x4 View, Matrix4x4 Projection, Vector2 Viewport)> cameraProvider)
{
_selectedGuidProvider = selectedGuidProvider;
_entityResolver = entityResolver;
_cameraProvider = cameraProvider;
}
///
/// Per-frame render call. No-op if nothing is selected, the selected
/// entity is gone, or the entity is off-screen / behind the camera.
/// Draws to the ImGui background draw list so it appears behind
/// other panels.
///
public void Render()
{
if (_selectedGuidProvider() is not uint guid) return;
if (_entityResolver(guid) is not TargetInfo info) return;
var (view, projection, viewport) = _cameraProvider();
if (viewport.X <= 0 || viewport.Y <= 0) return;
var viewProj = view * projection;
Vector2 tl, tr, br, bl;
if (info.WorldSphereCenter is Vector3 sphereCenter
&& info.WorldSphereRadius is float sphereRadius
&& AcDream.Core.Selection.ScreenProjection.TryProjectSphereToScreenRect(
sphereCenter, sphereRadius, view, projection, viewport,
out var rMin, out var rMax, out _,
minSidePixels: 12f))
{
// 2026-05-16 — retail-faithful path per
// SmartBox::GetObjectBoundingBox (decomp 0x00452e20).
// Retail uses CPhysicsObj::GetSelectionSphere (the Setup's
// baked selection_sphere) and produces the screen rect
// from that sphere's projection — NOT from a per-mesh AABB.
//
// Retail INFLATES the rect by one triangle width/height on
// every side before drawing (decomp 0x004f6a0b–0x004f6a99):
// edi_3 = arg4->left - eax_21 (shift left by triangleW)
// ebp_3 = arg4->top - eax_23 (shift up by triangleH)
// width = sphere_width + 2 * triangleW
// height = sphere_height + 2 * triangleH
// So the four corner triangles sit OUTSIDE the projected
// sphere by one triangle leg.
float ts = TriangleSize;
tl = new Vector2(rMin.X - ts, rMin.Y - ts);
tr = new Vector2(rMax.X + ts, rMin.Y - ts);
br = new Vector2(rMax.X + ts, rMax.Y + ts);
bl = new Vector2(rMin.X - ts, rMax.Y + ts);
}
else
{
// Fallback when the AABB isn't available (no setup cached
// yet, missing GfxObj bounds, behind the camera). Square
// box centred at the entity origin, height from the
// per-type heuristic.
if (!TryProjectToScreen(info.WorldPosition, viewProj, viewport, out var feetScreen))
return;
float entityHeight = EntityHeightFor(info.ItemType, info.ObjectDescriptionFlags, info.Scale, info.Useability);
var headWorld = new Vector3(
info.WorldPosition.X,
info.WorldPosition.Y,
info.WorldPosition.Z + entityHeight);
if (!TryProjectToScreen(headWorld, viewProj, viewport, out var headScreen))
return;
float screenHeight = MathF.Abs(headScreen.Y - feetScreen.Y);
if (screenHeight < MinScreenHeight) screenHeight = MinScreenHeight;
float screenWidth = screenHeight * WidthHeightRatio;
Vector2 center = (feetScreen + headScreen) * 0.5f;
float halfW = screenWidth * 0.5f;
float halfH = screenHeight * 0.5f;
tl = new Vector2(center.X - halfW, center.Y - halfH);
tr = new Vector2(center.X + halfW, center.Y - halfH);
br = new Vector2(center.X + halfW, center.Y + halfH);
bl = new Vector2(center.X - halfW, center.Y + halfH);
}
var rgba = RadarBlipColors.For(info.ItemType, info.ObjectDescriptionFlags);
uint col = MakeImGuiColor(rgba);
var drawList = ImGui.GetBackgroundDrawList();
float t = TriangleSize;
// 2026-05-16 — flipped per user feedback. Each corner triangle's
// RIGHT-ANGLE apex now points INWARD toward the target (was at
// the outer corner pointing outward). Combined with the
// TriangleSize inflate on the rect, the apex of each triangle
// lands at the projected mesh boundary while the hypotenuse
// runs across the outer (inflated) corner — giving the retail
// "corner-tick pointing at the entity" look.
//
// Geometry per corner:
// apex = corner + (±t, ±t) ← inward, right-angle here
// leg_a end = corner + (±t, 0) ← along horizontal edge
// leg_b end = corner + (0, ±t) ← along vertical edge
// Hypotenuse runs from leg_a end to leg_b end (the outer
// diagonal of the corner).
drawList.AddTriangleFilled(tl + new Vector2( t, t), tl + new Vector2( t, 0), tl + new Vector2(0, t), col);
drawList.AddTriangleFilled(tr + new Vector2(-t, t), tr + new Vector2(-t, 0), tr + new Vector2(0, t), col);
drawList.AddTriangleFilled(br + new Vector2(-t, -t), br + new Vector2(-t, 0), br + new Vector2(0, -t), col);
drawList.AddTriangleFilled(bl + new Vector2( t, -t), bl + new Vector2( t, 0), bl + new Vector2(0, -t), col);
}
///
/// Project a world-space point to screen-space pixels. Returns
/// false if the point is behind the camera or outside the
/// expanded viewport (±20 % margin so a tall entity whose feet are
/// just off the bottom of the screen still gets its head projected).
///
private static bool TryProjectToScreen(
Vector3 world,
Matrix4x4 viewProj,
Vector2 viewport,
out Vector2 screen)
{
var clip = Vector4.Transform(new Vector4(world, 1f), viewProj);
if (clip.W <= 0.001f)
{
screen = Vector2.Zero;
return false;
}
float ndcX = clip.X / clip.W;
float ndcY = clip.Y / clip.W;
const float margin = 1.2f;
if (ndcX < -margin || ndcX > margin || ndcY < -margin || ndcY > margin)
{
screen = Vector2.Zero;
return false;
}
screen = new Vector2(
(ndcX * 0.5f + 0.5f) * viewport.X,
(1f - (ndcY * 0.5f + 0.5f)) * viewport.Y);
return true;
}
private static uint MakeImGuiColor(RadarBlipColors.Rgba c)
{
// ImGui packed colour is 0xAABBGGRR (little-endian RGBA).
return ((uint)c.A << 24) | ((uint)c.B << 16) | ((uint)c.G << 8) | c.R;
}
}