diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 8b867e0..1831713 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -1801,10 +1801,10 @@ public sealed class GameWindow : IDisposable partIndex++; } - // If no BSP parts were registered, check for CylSphere collision - // from the Setup (trees, rocks, NPCs use cylinder collision). - if (partIndex == 0 || !entity.MeshRefs.Any(mr => - _physicsDataCache.GetGfxObj(mr.GfxObjId)?.BSP?.Root is not null)) + // ALWAYS register CylSphere when the Setup has one, even if BSP parts + // were also registered. Retail tests both: CylSphere as the broad + // collision volume (trunk) plus BSP parts for precise polygon collision. + // The CylSphere catches the base/trunk that BSP parts might miss. { var setup = _physicsDataCache.GetSetup(entity.SourceGfxObjOrSetupId); if (setup is not null && setup.CylSpheres.Count > 0) @@ -1813,6 +1813,7 @@ public sealed class GameWindow : IDisposable float cylRadius = cyl.Radius > 0 ? cyl.Radius : setup.Radius; if (cylRadius > 0) { + // Use entity.Id directly (not partId) for the CylSphere entry. _physicsEngine.ShadowObjects.Register( entity.Id, entity.SourceGfxObjOrSetupId, entity.Position + new System.Numerics.Vector3(cyl.Origin.X, cyl.Origin.Y, cyl.Origin.Z), @@ -1821,8 +1822,9 @@ public sealed class GameWindow : IDisposable AcDream.Core.Physics.ShadowCollisionType.Cylinder, cyl.Height); } } - else if (setup is not null && setup.Spheres.Count > 0) + else if (setup is not null && setup.Spheres.Count > 0 && partIndex == 0) { + // Fallback: use bounding sphere only if no BSP and no CylSphere. var sph = setup.Spheres[0]; if (sph.Radius > 0) {