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>
150 lines
6.1 KiB
C#
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);
|
|
}
|
|
}
|