using System; using System.Collections.Generic; using System.Numerics; using AcDream.Core.Physics; using Xunit; namespace AcDream.Core.Tests.Physics; /// /// Indoor walking Phase D (2026-05-19): tests for the indoor-cell-containment /// check added to . /// Covers the four scenarios described in the Phase D implementation plan. /// public class ResolveOutdoorCellIdIndoorContainmentTests { /// /// Build a whose local AABB spans ± /// around the origin, placed at via the /// WorldTransform / InverseWorldTransform pair. /// private static CellPhysics MakeIndoorCellAt(Vector3 worldOrigin, Vector3 halfExtent) { // Four vertices defining a floor quad — enough for AABB computation at // cache time (in production this is done by CacheCellStruct, in tests // we pre-supply LocalAabbMin / LocalAabbMax directly). var min = -halfExtent; var max = halfExtent; var verts = new[] { new Vector3(min.X, min.Y, min.Z), new Vector3(max.X, min.Y, min.Z), new Vector3(max.X, max.Y, max.Z), new Vector3(min.X, max.Y, max.Z), }; var poly = new ResolvedPolygon { Vertices = verts, Plane = new Plane(Vector3.UnitZ, 0f), NumPoints = 4, SidesType = DatReaderWriter.Enums.CullMode.None, }; var world = Matrix4x4.CreateTranslation(worldOrigin); Matrix4x4.Invert(world, out var inv); return new CellPhysics { Resolved = new Dictionary { [0] = poly }, WorldTransform = world, InverseWorldTransform = inv, }; } // ----------------------------------------------------------------------- // Test 1: player inside a cached EnvCell → returns that cell's full id. // ----------------------------------------------------------------------- [Fact] public void ResolveOutdoorCellId_PlayerInsideCachedEnvCell_ReturnsEnvCellId() { var engine = new PhysicsEngine(); engine.DataCache = new PhysicsDataCache(); // Cache an EnvCell at world origin spanning ±5 m on each axis. var cell = MakeIndoorCellAt(Vector3.Zero, new Vector3(5f, 5f, 5f)); engine.DataCache.RegisterCellStructForTest(0xA9B40172u, cell); // Player at world origin → inside the EnvCell's AABB. uint result = engine.ResolveOutdoorCellId(Vector3.Zero, fallbackCellId: 0x00000031u); Assert.Equal(0xA9B40172u, result); } // ----------------------------------------------------------------------- // Test 2: player outside all cached EnvCells → falls through to outdoor // (and since no landblocks are registered, returns the fallback unchanged). // ----------------------------------------------------------------------- [Fact] public void ResolveOutdoorCellId_PlayerOutsideAllCachedEnvCells_FallsThroughToOutdoor() { var engine = new PhysicsEngine(); engine.DataCache = new PhysicsDataCache(); var cell = MakeIndoorCellAt(Vector3.Zero, new Vector3(5f, 5f, 5f)); engine.DataCache.RegisterCellStructForTest(0xA9B40172u, cell); // Player at (100, 100, 0) — far outside the cached EnvCell. // No landblocks registered → outdoor branch can't match either. uint result = engine.ResolveOutdoorCellId(new Vector3(100f, 100f, 0f), fallbackCellId: 0x00000031u); Assert.Equal(0x00000031u, result); } // ----------------------------------------------------------------------- // Test 3: EnvCell with a non-identity WorldTransform (rotation around Z). // Player at world (3, 0, 0) is still inside the rotated local AABB. // ----------------------------------------------------------------------- [Fact] public void ResolveOutdoorCellId_PlayerInsideEnvCellWithRotatedTransform_StillDetectsContainment() { var halfExtent = new Vector3(5f, 5f, 5f); var verts = new[] { new Vector3(-5f, -5f, -5f), new Vector3( 5f, -5f, -5f), new Vector3( 5f, 5f, 5f), new Vector3(-5f, 5f, 5f), }; var poly = new ResolvedPolygon { Vertices = verts, Plane = new Plane(Vector3.UnitZ, 0f), NumPoints = 4, SidesType = DatReaderWriter.Enums.CullMode.None, }; // 90° rotation around Z. A point at world (3, 0, 0) transforms to // local (0, -3, 0) — still within ±5 on every axis. var rotation = Matrix4x4.CreateRotationZ(MathF.PI / 2f); Matrix4x4.Invert(rotation, out var inv); var cell = new CellPhysics { Resolved = new Dictionary { [0] = poly }, WorldTransform = rotation, InverseWorldTransform = inv, }; var engine = new PhysicsEngine(); engine.DataCache = new PhysicsDataCache(); engine.DataCache.RegisterCellStructForTest(0xA9B40172u, cell); uint result = engine.ResolveOutdoorCellId(new Vector3(3f, 0f, 0f), fallbackCellId: 0x00000031u); Assert.Equal(0xA9B40172u, result); } // ----------------------------------------------------------------------- // Test 4: fallbackCellId == 0 → always returns 0 (existing early-return). // ----------------------------------------------------------------------- [Fact] public void ResolveOutdoorCellId_FallbackZero_ReturnsZero() { var engine = new PhysicsEngine(); engine.DataCache = new PhysicsDataCache(); // Even if the player is inside a cell, fallback=0 should still return 0. var cell = MakeIndoorCellAt(Vector3.Zero, new Vector3(5f, 5f, 5f)); engine.DataCache.RegisterCellStructForTest(0xA9B40172u, cell); uint result = engine.ResolveOutdoorCellId(Vector3.Zero, fallbackCellId: 0u); Assert.Equal(0u, result); } }