diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs
index c5a4b5c..134fa38 100644
--- a/src/AcDream.App/Rendering/GameWindow.cs
+++ b/src/AcDream.App/Rendering/GameWindow.cs
@@ -8991,13 +8991,47 @@ public sealed class GameWindow : IDisposable
origin, direction,
_entitiesByServerGuid.Values,
skipServerGuid: _playerServerGuid,
- maxDistance: 50f);
+ maxDistance: 50f,
+ // B.7 (2026-05-15): widen the pick sphere for large flat
+ // objects (doors, lifestones, portals, corpses) so their
+ // visible surface stays clickable even though the entity
+ // origin is a single point. 0.7 m default is fine for
+ // humanoids and most items; doors / portals need ~2 m
+ // to cover the doorframe.
+ radiusForGuid: g =>
+ {
+ if (_lastSpawnByGuid.TryGetValue(g, out var s)
+ && s.ObjectDescriptionFlags is { } odf)
+ {
+ // BF_DOOR = 0x1000, BF_LIFESTONE = 0x4000,
+ // BF_PORTAL = 0x40000, BF_CORPSE = 0x2000
+ // (acclient.h:6431-6463)
+ const uint LargeFlatMask = 0x1000u | 0x4000u | 0x40000u | 0x2000u;
+ if ((odf & LargeFlatMask) != 0) return 2.0f;
+ }
+ return 0.7f;
+ });
if (picked is uint guid)
{
_selectedGuid = guid;
string label = DescribeLiveEntity(guid);
Console.WriteLine($"[B.4b] pick guid=0x{guid:X8} name={label}");
+ // B.7 (2026-05-15): one-shot per-pick diagnostic so we can
+ // see exactly which ItemType + PWD bitfield bits + resolved
+ // RadarBlipColor are produced for the just-picked entity.
+ // Helps verify whether a "green NPC" really is flagged as
+ // Vendor server-side or whether our lookup is wrong.
+ uint rawItemType = 0;
+ if (_liveEntityInfoByGuid.TryGetValue(guid, out var info))
+ rawItemType = (uint)info.ItemType;
+ uint pwdBits = 0;
+ if (_lastSpawnByGuid.TryGetValue(guid, out var spawn)
+ && spawn.ObjectDescriptionFlags is { } odf)
+ pwdBits = odf;
+ var col = AcDream.Core.Ui.RadarBlipColors.For(rawItemType, pwdBits);
+ Console.WriteLine(System.FormattableString.Invariant(
+ $"[B.7] pick-info guid=0x{guid:X8} itemType=0x{rawItemType:X8} pwd=0x{pwdBits:X8} color=({col.R},{col.G},{col.B})"));
_debugVm?.AddToast($"Selected: {label}");
if (useImmediately) SendUse(guid);
}
diff --git a/src/AcDream.App/UI/TargetIndicatorPanel.cs b/src/AcDream.App/UI/TargetIndicatorPanel.cs
index 18bc025..3b63148 100644
--- a/src/AcDream.App/UI/TargetIndicatorPanel.cs
+++ b/src/AcDream.App/UI/TargetIndicatorPanel.cs
@@ -65,10 +65,12 @@ public sealed class TargetIndicatorPanel
///
/// Box width = projected height ×
- /// . Humanoids are about half as
- /// wide as tall on screen, so 0.5 is a sensible default.
+ /// . 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; } = 0.5f;
+ public float WidthHeightRatio { get; set; } = 1.0f;
///
/// Floor for the projected screen height (pixels). Prevents the
diff --git a/src/AcDream.Core/Selection/WorldPicker.cs b/src/AcDream.Core/Selection/WorldPicker.cs
index 6074373..0d55dda 100644
--- a/src/AcDream.Core/Selection/WorldPicker.cs
+++ b/src/AcDream.Core/Selection/WorldPicker.cs
@@ -89,10 +89,10 @@ public static class WorldPicker
Vector3 origin, Vector3 direction,
IEnumerable candidates,
uint skipServerGuid,
- float maxDistance = 50f)
+ float maxDistance = 50f,
+ Func? radiusForGuid = null)
{
- const float Radius = 0.7f;
- const float Radius2 = Radius * Radius;
+ const float DefaultRadius = 0.7f;
if (direction.LengthSquared() < 1e-10f) return null;
@@ -103,13 +103,20 @@ public static class WorldPicker
if (entity.ServerGuid == 0u) continue;
if (entity.ServerGuid == skipServerGuid) continue;
+ // Per-entity radius (caller-supplied) lets large flat objects
+ // like doors, lifestones, and portals use a bigger sphere
+ // than the 0.7 m humanoid/item default — their visible
+ // surface extends well beyond their origin point.
+ float r = radiusForGuid?.Invoke(entity.ServerGuid) ?? DefaultRadius;
+ float r2 = r * r;
+
// Geometric ray-sphere: oc = origin - center, b = dot(oc, dir),
// c = |oc|^2 - r^2, discriminant = b^2 - c. If discriminant < 0
// the ray misses the sphere. Otherwise nearest intersection is
// t = -b - sqrt(discriminant).
var oc = origin - entity.Position;
float b = Vector3.Dot(oc, direction);
- float c = Vector3.Dot(oc, oc) - Radius2;
+ float c = Vector3.Dot(oc, oc) - r2;
float d = b * b - c;
if (d < 0f) continue;