diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 134fa38..60a3dd5 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -9009,7 +9009,28 @@ public sealed class GameWindow : IDisposable const uint LargeFlatMask = 0x1000u | 0x4000u | 0x40000u | 0x2000u; if ((odf & LargeFlatMask) != 0) return 2.0f; } - return 0.7f; + // 1.0 m sphere centred at chest height (see + // verticalOffsetForGuid) covers a 1.8 m humanoid from + // shin to crown without overlapping neighbours. + return 1.0f; + }, + verticalOffsetForGuid: g => + { + // Lift the pick sphere to mid-body so chest/head clicks + // hit instead of missing past the top of a feet-anchored + // sphere. WorldEntity.Position is at feet level + // (Z=ground); humanoids reach ~1.8 m, items sit close + // to the ground (~0.2 m above their feet). + if (_liveEntityInfoByGuid.TryGetValue(g, out var info) + && (info.ItemType & AcDream.Core.Items.ItemType.Creature) != 0) + return 0.9f; // humanoid mid-body + if (_lastSpawnByGuid.TryGetValue(g, out var s) + && s.ObjectDescriptionFlags is { } odf) + { + const uint LargeFlatMask = 0x1000u | 0x4000u | 0x40000u | 0x2000u; + if ((odf & LargeFlatMask) != 0) return 1.0f; // mid-door + } + return 0.2f; // small ground item — sphere just above feet }); if (picked is uint guid) diff --git a/src/AcDream.Core/Selection/WorldPicker.cs b/src/AcDream.Core/Selection/WorldPicker.cs index 0d55dda..dfc0300 100644 --- a/src/AcDream.Core/Selection/WorldPicker.cs +++ b/src/AcDream.Core/Selection/WorldPicker.cs @@ -90,9 +90,11 @@ public static class WorldPicker IEnumerable candidates, uint skipServerGuid, float maxDistance = 50f, - Func? radiusForGuid = null) + Func? radiusForGuid = null, + Func? verticalOffsetForGuid = null) { - const float DefaultRadius = 0.7f; + const float DefaultRadius = 1.0f; + const float DefaultVerticalOffset = 0.9f; if (direction.LengthSquared() < 1e-10f) return null; @@ -103,18 +105,38 @@ 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. + // Per-entity radius + vertical offset (caller-supplied). + // + // + // Vertical offset (2026-05-15). WorldEntity.Position + // is at the entity's feet (Z=ground for a humanoid). User + // clicks usually land on chest/head (Z ≈ 1–1.8 m). With the + // sphere centred at feet, a chest click is 1.2 m of vertical + // distance from sphere centre — bigger than any reasonable + // body radius — so the ray misses. Lifting the sphere + // centre to mid-body fixes this: 0.9 m default for + // humanoids, smaller for items, larger for tall objects. + // + // + // + // Radius (2026-05-15). Bumped default 0.7 → 1.0 m to + // accommodate the new vertical-offset sphere placement + // (chest-height sphere centre + 1.0 m radius covers from + // shin to top-of-head for a 1.8 m humanoid). + // float r = radiusForGuid?.Invoke(entity.ServerGuid) ?? DefaultRadius; float r2 = r * r; + float vz = verticalOffsetForGuid?.Invoke(entity.ServerGuid) ?? DefaultVerticalOffset; + var sphereCenter = new Vector3( + entity.Position.X, + entity.Position.Y, + entity.Position.Z + vz); // 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; + var oc = origin - sphereCenter; float b = Vector3.Dot(oc, direction); float c = Vector3.Dot(oc, oc) - r2; float d = b * b - c;