acdream/tests/AcDream.Core.Tests/Physics/ResolveOutdoorCellIdTests.cs
Erik 1969c55823 feat(physics): Phase 2 — wire CellBSP + Portals into CellPhysics
Adds PortalInfo struct and extends CellPhysics with CellBSP (third BSP
for point-in-cell tests, typed CellBSPTree from DatReaderWriter),
Portals (from envCell.CellPortals), PortalPolygons (resolved
cellStruct.Polygons — portals reference visible polys, not
PhysicsPolygons), and VisibleCellIds (populated for future use;
envCell.VisibleCells is List<UInt16>, not Dictionary).

Deletes CellPhysics.LocalAabbMin/Max and PhysicsDataCache.TryFindContainingCell
— Phase D's AABB shortcut is gone. CacheCellStruct's AABB compute
removed; the [cell-cache] diagnostic updated with portal/visible counts
instead.

CacheCellStruct signature gains an EnvCell parameter (one call site in
GameWindow.cs:5384 updated). ResolveOutdoorCellId drops the
TryFindContainingCell call; portal-graph CellTransit replaces it next.

ResolveOutdoorCellIdTests object initializers had the deleted AABB
properties stripped temporarily so the build stays green; the file gets
replaced wholesale in the next commit (CellTransit integration). Those
2 AABB-containment tests continue to fail (they were pre-broken on this
branch); no new failures introduced.

Spec: docs/superpowers/specs/2026-05-19-indoor-portal-cell-tracking-design.md
Plan: docs/superpowers/plans/2026-05-19-indoor-portal-cell-tracking.md

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 16:52:20 +02:00

150 lines
6.1 KiB
C#

using System;
using System.Collections.Generic;
using System.Numerics;
using AcDream.Core.Physics;
using Xunit;
namespace AcDream.Core.Tests.Physics;
/// <summary>
/// Indoor walking Phase D (2026-05-19): tests for the indoor-cell-containment
/// check added to <see cref="PhysicsEngine.ResolveOutdoorCellId"/>.
/// Covers the four scenarios described in the Phase D implementation plan.
/// </summary>
public class ResolveOutdoorCellIdIndoorContainmentTests
{
/// <summary>
/// Build a <see cref="CellPhysics"/> whose local AABB spans ±<paramref name="halfExtent"/>
/// around the origin, placed at <paramref name="worldOrigin"/> via the
/// WorldTransform / InverseWorldTransform pair.
/// </summary>
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<ushort, ResolvedPolygon> { [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<ushort, ResolvedPolygon> { [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);
}
}