acdream/tests/AcDream.Core.Tests/Physics/RetailStepDownPlacementTests.cs

271 lines
8.7 KiB
C#

using System.Collections.Generic;
using System.Numerics;
using AcDream.Core.Physics;
using DatReaderWriter.Enums;
using DatReaderWriter.Types;
using Xunit;
namespace AcDream.Core.Tests.Physics;
/// <summary>
/// Pins retail <c>CTransition::step_down</c> (0x0050B2A0): every
/// successfully supported candidate is re-tested with
/// <see cref="InsertType.Placement"/>, regardless of whether the caller is
/// ordinary contact maintenance or StepUp.
/// </summary>
public sealed class RetailStepDownPlacementTests
{
private const uint Cell = 0xA9B40001u;
private const float Radius = 0.48f;
[Theory]
[InlineData(false)]
[InlineData(true)]
public void OrdinaryContactMaintenance_AlwaysRunsFinalPlacement(bool twoSpheres)
{
Transition transition = MakeGroundedTransition(twoSpheres);
int supportPasses = 0;
int placementPasses = 0;
var engine = new PhysicsEngine
{
TransitionCellCollisionTestHook = (candidate, phase, _, actual) =>
{
if (phase != TransitionCellCollisionPhase.Environment)
return actual;
if (candidate.SpherePath.StepDown)
{
supportPasses++;
candidate.CollisionInfo.SetContactPlane(
new Plane(Vector3.UnitZ, 0f), Cell);
}
else if (candidate.SpherePath.InsertType == InsertType.Placement)
{
placementPasses++;
}
return actual;
},
};
TransitionState result = transition.TransitionalInsertForTest(1, engine);
Assert.Equal(TransitionState.OK, result);
Assert.Equal(1, supportPasses);
Assert.Equal(1, placementPasses);
Assert.Equal(InsertType.Transition, transition.SpherePath.InsertType);
Assert.False(transition.SpherePath.StepDown);
}
[Fact]
public void SupportedCandidate_OverlappingDuringPlacement_IsRejected()
{
Transition transition = MakeGroundedTransition(twoSpheres: true);
int placementPasses = 0;
var engine = new PhysicsEngine
{
TransitionCellCollisionTestHook = (candidate, phase, _, actual) =>
{
if (phase != TransitionCellCollisionPhase.Environment)
return actual;
if (candidate.SpherePath.StepDown)
{
candidate.CollisionInfo.SetContactPlane(
new Plane(Vector3.UnitZ, 0f), Cell);
return actual;
}
if (candidate.SpherePath.InsertType == InsertType.Placement)
{
placementPasses++;
return TransitionState.Collided;
}
return actual;
},
};
bool accepted = transition.DoStepDownForTest(
stepDownHeight: 0.04f,
walkableZ: PhysicsGlobals.FloorZ,
engine);
Assert.False(accepted);
Assert.Equal(1, placementPasses);
Assert.Equal(InsertType.Transition, transition.SpherePath.InsertType);
Assert.False(transition.SpherePath.StepDown);
}
[Fact]
public void StepUp_UsesTheSameFinalPlacementPass()
{
Transition transition = MakeGroundedTransition(twoSpheres: true);
int placementPasses = 0;
var engine = new PhysicsEngine
{
TransitionCellCollisionTestHook = (candidate, phase, _, actual) =>
{
if (phase != TransitionCellCollisionPhase.Environment)
return actual;
if (candidate.SpherePath.StepDown)
{
candidate.CollisionInfo.SetContactPlane(
new Plane(Vector3.UnitZ, 0f), Cell);
}
else if (candidate.SpherePath.InsertType == InsertType.Placement)
{
placementPasses++;
}
return actual;
},
};
bool accepted = transition.DoStepUp(Vector3.UnitX, engine);
Assert.True(accepted);
Assert.Equal(1, placementPasses);
Assert.Equal(InsertType.Transition, transition.SpherePath.InsertType);
Assert.False(transition.SpherePath.StepUp);
Assert.False(transition.SpherePath.StepDown);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void PlacementDispatcher_AllowsExactWallTangency_AndRejectsOverlap(
bool twoSpheres)
{
(PhysicsBSPNode root, Dictionary<ushort, ResolvedPolygon> resolved) =
BuildWall();
FlatPhysicsBsp flat =
FlatCollisionAssetBuilder.FlattenPhysicsBsp(root, resolved);
float tangentY = Radius;
TransitionState graphTangent = RunPlacement(
root, resolved, flat: null, tangentY, twoSpheres);
TransitionState flatTangent = RunPlacement(
root: null, resolved, flat, tangentY, twoSpheres);
TransitionState graphOverlap = RunPlacement(
root, resolved, flat: null,
Radius - PhysicsGlobals.EPSILON * 2f,
twoSpheres);
TransitionState flatOverlap = RunPlacement(
root: null, resolved, flat,
Radius - PhysicsGlobals.EPSILON * 2f,
twoSpheres);
Assert.Equal(TransitionState.OK, graphTangent);
Assert.Equal(graphTangent, flatTangent);
Assert.Equal(TransitionState.Collided, graphOverlap);
Assert.Equal(graphOverlap, flatOverlap);
}
private static TransitionState RunPlacement(
PhysicsBSPNode? root,
Dictionary<ushort, ResolvedPolygon> resolved,
FlatPhysicsBsp? flat,
float centerY,
bool twoSpheres)
{
var foot = new Sphere
{
Origin = new Vector3(0f, centerY, 0f),
Radius = Radius,
};
Sphere? head = twoSpheres
? new Sphere
{
Origin = new Vector3(0f, centerY, 0.875f),
Radius = Radius,
}
: null;
var transition = new Transition();
transition.SpherePath.InitPath(
begin: Vector3.Zero,
end: Vector3.Zero,
Cell,
Radius,
sphereHeight: twoSpheres ? 1.355f : 0f);
transition.SpherePath.InsertType = InsertType.Placement;
return flat is null
? BSPQuery.FindCollisions(
root,
resolved,
transition,
foot,
head,
foot.Origin,
Vector3.UnitZ,
1f)
: FlatBspQuery.FindCollisions(
flat,
transition,
foot,
head,
foot.Origin,
Vector3.UnitZ,
1f);
}
private static Transition MakeGroundedTransition(bool twoSpheres)
{
Vector3 current = new(2f, 3f, 4f);
Vector3 target = current + new Vector3(0.1f, 0f, 0f);
var transition = new Transition();
transition.SpherePath.InitPath(
current,
target,
Cell,
Radius,
sphereHeight: twoSpheres ? 1.835f : 0f);
transition.SpherePath.SetCheckPos(target, Cell);
transition.ObjectInfo.State =
ObjectInfoState.Contact | ObjectInfoState.OnWalkable;
transition.ObjectInfo.StepDown = true;
transition.ObjectInfo.StepDownHeight = 0.04f;
transition.ObjectInfo.StepUpHeight = 0.60f;
transition.CollisionInfo.LastKnownContactPlane =
new Plane(Vector3.UnitZ, 0f);
transition.CollisionInfo.LastKnownContactPlaneValid = true;
return transition;
}
private static (
PhysicsBSPNode Root,
Dictionary<ushort, ResolvedPolygon> Resolved) BuildWall()
{
Vector3[] vertices =
[
new(-2f, 0f, -2f),
new(-2f, 0f, 2f),
new( 2f, 0f, 2f),
new( 2f, 0f, -2f),
];
var root = new PhysicsBSPNode
{
Type = BSPNodeType.Leaf,
BoundingSphere = new Sphere
{
Origin = Vector3.Zero,
Radius = 4f,
},
};
root.Polygons.Add(1);
var resolved = new Dictionary<ushort, ResolvedPolygon>
{
[1] = new ResolvedPolygon
{
Id = 1,
Vertices = vertices,
Plane = new Plane(Vector3.UnitY, 0f),
NumPoints = vertices.Length,
SidesType = CullMode.None,
},
};
return (root, resolved);
}
}