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;