using System; using System.Numerics; using AcDream.Core.Selection; using AcDream.Core.World; using Xunit; namespace AcDream.Core.Tests.Selection; public class WorldPickerTests { private const float Epsilon = 0.01f; private static (Matrix4x4 View, Matrix4x4 Projection) MakeIdentityCamera() { var view = Matrix4x4.Identity; var proj = Matrix4x4.CreatePerspectiveFieldOfView( fieldOfView: MathF.PI / 3f, aspectRatio: 16f / 9f, nearPlaneDistance: 0.1f, farPlaneDistance: 100f); return (view, proj); } [Fact] public void BuildRay_CenterOfViewport_ReturnsForwardRay() { var (view, proj) = MakeIdentityCamera(); const float vpW = 1920f, vpH = 1080f; var (_, direction) = WorldPicker.BuildRay( mouseX: vpW / 2f, mouseY: vpH / 2f, viewportW: vpW, viewportH: vpH, view, proj); // Right-handed perspective + identity view -> camera looks down -Z. // Center pixel ray = (0, 0, -1) within float epsilon. Assert.True(MathF.Abs(direction.X) < Epsilon, $"direction.X = {direction.X}"); Assert.True(MathF.Abs(direction.Y) < Epsilon, $"direction.Y = {direction.Y}"); Assert.True(direction.Z < -0.99f, $"direction.Z = {direction.Z}"); } [Fact] public void BuildRay_OffsetMouseRight_DeflectsRayPositiveX() { var (view, proj) = MakeIdentityCamera(); const float vpW = 1920f, vpH = 1080f; var (_, direction) = WorldPicker.BuildRay( mouseX: vpW * 0.75f, mouseY: vpH / 2f, viewportW: vpW, viewportH: vpH, view, proj); Assert.True(direction.X > 0.1f, $"direction.X = {direction.X} (expected > 0.1)"); } private static WorldEntity MakeEntity(uint serverGuid, Vector3 position) => new() { Id = serverGuid == 0u ? 1u : serverGuid, ServerGuid = serverGuid, SourceGfxObjOrSetupId = 0u, Position = position, Rotation = Quaternion.Identity, MeshRefs = Array.Empty(), }; [Fact] public void Pick_RayThroughEntity_ReturnsServerGuid() { var entity = MakeEntity(0xABCDu, new Vector3(0, 0, -10)); var result = WorldPicker.Pick( origin: Vector3.Zero, direction: -Vector3.UnitZ, candidates: new[] { entity }, skipServerGuid: 0u); Assert.Equal(0xABCDu, result); } [Fact] public void Pick_RayMisses_ReturnsNull() { var entity = MakeEntity(0xABCDu, new Vector3(0, 0, -10)); var result = WorldPicker.Pick( origin: Vector3.Zero, direction: Vector3.UnitX, candidates: new[] { entity }, skipServerGuid: 0u); Assert.Null(result); } [Fact] public void Pick_TwoEntitiesInLine_ReturnsCloser() { var near = MakeEntity(0x1111u, new Vector3(0, 0, -5)); var far = MakeEntity(0x2222u, new Vector3(0, 0, -20)); var result = WorldPicker.Pick( origin: Vector3.Zero, direction: -Vector3.UnitZ, candidates: new[] { far, near }, // iteration order shouldn't matter skipServerGuid: 0u); Assert.Equal(0x1111u, result); } [Fact] public void Pick_SkipsSkipGuid() { var entity = MakeEntity(0xABCDu, new Vector3(0, 0, -10)); var result = WorldPicker.Pick( origin: Vector3.Zero, direction: -Vector3.UnitZ, candidates: new[] { entity }, skipServerGuid: 0xABCDu); Assert.Null(result); } [Fact] public void Pick_SkipsZeroServerGuid() { // Atlas-tier scenery / dat-hydrated statics carry ServerGuid=0 // and aren't valid Use targets — server would reject guid=0. var entity = MakeEntity(0u, new Vector3(0, 0, -10)); var result = WorldPicker.Pick( origin: Vector3.Zero, direction: -Vector3.UnitZ, candidates: new[] { entity }, skipServerGuid: 0xDEADu); Assert.Null(result); } [Fact] public void Pick_BeyondMaxDistance_ReturnsNull() { var entity = MakeEntity(0xABCDu, new Vector3(0, 0, -100)); var result = WorldPicker.Pick( origin: Vector3.Zero, direction: -Vector3.UnitZ, candidates: new[] { entity }, skipServerGuid: 0u); // default maxDistance = 50f Assert.Null(result); } [Fact] public void Pick_RayOriginInsideEntitySphere_StillReturnsServerGuid() { // Player ~3m from a door -> camera near-plane sits INSIDE the door's // 5m bounding sphere. Naive t_near < 0 guard would skip; correct // behavior is to fall through to t_far (the sphere exit point). var entity = MakeEntity(0xABCDu, new Vector3(0, 0, -3)); var result = WorldPicker.Pick( origin: Vector3.Zero, direction: -Vector3.UnitZ, candidates: new[] { entity }, skipServerGuid: 0u); Assert.Equal(0xABCDu, result); } }