using System; using System.Collections.Generic; using System.Numerics; using AcDream.Core.Physics; using Xunit; namespace AcDream.Core.Tests.Physics; public class PhysicsEngineTests { private static float[] LinearHeightTable() { var table = new float[256]; for (int i = 0; i < 256; i++) table[i] = i * 1.0f; return table; } private static byte[] FlatHeightmap(byte value = 50) { var heights = new byte[81]; Array.Fill(heights, value); return heights; } private PhysicsEngine MakeFlatEngine(float terrainZ = 50f) { var engine = new PhysicsEngine(); var terrain = new TerrainSurface(FlatHeightmap((byte)terrainZ), LinearHeightTable()); engine.AddLandblock(0xA9B4FFFFu, terrain, Array.Empty(), worldOffsetX: 0f, worldOffsetY: 0f); return engine; } [Fact] public void Resolve_FlatTerrain_ZMatchesTerrain() { var engine = MakeFlatEngine(terrainZ: 50f); var result = engine.Resolve( new Vector3(96f, 96f, 50f), cellId: 0x0001, delta: new Vector3(1f, 0f, 0f), stepUpHeight: 2f); Assert.Equal(50f, result.Position.Z, precision: 1); Assert.True(result.IsOnGround); } [Fact] public void Resolve_WalkUpSmallSlope_Accepted() { // Heights slope from 50 to 52 across X — small enough for step height. var heights = new byte[81]; for (int x = 0; x < 9; x++) for (int y = 0; y < 9; y++) heights[x * 9 + y] = (byte)(50 + x / 4); // gentle slope var engine = new PhysicsEngine(); var terrain = new TerrainSurface(heights, LinearHeightTable()); engine.AddLandblock(0xA9B4FFFFu, terrain, Array.Empty(), worldOffsetX: 0f, worldOffsetY: 0f); var result = engine.Resolve( new Vector3(48f, 96f, 50f), cellId: 0x0001, delta: new Vector3(48f, 0f, 0f), stepUpHeight: 5f); Assert.True(result.IsOnGround); Assert.True(result.Position.Z >= 50f); // moved uphill } [Fact] public void Resolve_StepUpExceedsHeight_MovementBlocked() { // Heights jump sharply: left half = 50, right half = 100. var heights = new byte[81]; for (int x = 0; x < 9; x++) for (int y = 0; y < 9; y++) heights[x * 9 + y] = (byte)(x < 5 ? 50 : 100); var engine = new PhysicsEngine(); var terrain = new TerrainSurface(heights, LinearHeightTable()); engine.AddLandblock(0xA9B4FFFFu, terrain, Array.Empty(), worldOffsetX: 0f, worldOffsetY: 0f); // Try to walk from the low side to the high side. var result = engine.Resolve( new Vector3(96f, 96f, 50f), cellId: 0x0001, delta: new Vector3(48f, 0f, 0f), stepUpHeight: 2f); // Movement should be blocked — Z delta (50→100) exceeds step height (2). Assert.Equal(96f, result.Position.X, precision: 1); // didn't move Assert.True(result.IsOnGround); } [Fact] public void Resolve_EnterIndoorCell_StaysOutdoor_BecauseTransitionDisabled() { // Phase B.2 MVP: outdoor→indoor transitions are disabled because // CellSurface floor polygons are too aggressive (building // footprints/roofs capture the player). Walking over a cell's XY // area stays on the outdoor terrain Z. Indoor transitions will be // re-enabled when portal-based detection lands in Phase E. var engine = new PhysicsEngine(); var terrain = new TerrainSurface(FlatHeightmap(50), LinearHeightTable()); var cellVerts = new Dictionary { [0] = new(40f, 40f, 55f), [1] = new(60f, 40f, 55f), [2] = new(60f, 60f, 55f), [3] = new(40f, 60f, 55f), }; var cellPolys = new List> { new() { 0, 1, 2, 3 } }; var cell = new CellSurface(0x0100, cellVerts, cellPolys); engine.AddLandblock(0xA9B4FFFFu, terrain, new[] { cell }, worldOffsetX: 0f, worldOffsetY: 0f); // Walk from outdoor (30, 50) into the cell's floor area (50, 50). var result = engine.Resolve( new Vector3(30f, 50f, 50f), cellId: 0x0001, delta: new Vector3(20f, 0f, 0f), stepUpHeight: 10f); // Should stay outdoor (transition disabled) at terrain Z = 50. Assert.True(result.CellId < 0x0100u); Assert.Equal(50f, result.Position.Z, precision: 1); Assert.True(result.IsOnGround); } [Fact] public void Resolve_LeaveIndoorCell_TransitionsToOutdoor() { var engine = new PhysicsEngine(); var terrain = new TerrainSurface(FlatHeightmap(50), LinearHeightTable()); var cellVerts = new Dictionary { [0] = new(40f, 40f, 55f), [1] = new(60f, 40f, 55f), [2] = new(60f, 60f, 55f), [3] = new(40f, 60f, 55f), }; var cellPolys = new List> { new() { 0, 1, 2, 3 } }; var cell = new CellSurface(0x0100, cellVerts, cellPolys); engine.AddLandblock(0xA9B4FFFFu, terrain, new[] { cell }, worldOffsetX: 0f, worldOffsetY: 0f); // Start inside the cell, walk out. var result = engine.Resolve( new Vector3(50f, 50f, 55f), cellId: 0x0100, delta: new Vector3(-20f, 0f, 0f), stepUpHeight: 10f); // Should transition back to outdoor. Assert.True(result.CellId < 0x0100u); Assert.Equal(50f, result.Position.Z, precision: 1); Assert.True(result.IsOnGround); } [Fact] public void Resolve_NoSurfaceUnderEntity_NotOnGround() { var engine = new PhysicsEngine(); // No landblocks loaded — entity is floating in void. var result = engine.Resolve( new Vector3(0f, 0f, 100f), cellId: 0x0001, delta: Vector3.Zero, stepUpHeight: 2f); Assert.False(result.IsOnGround); } }