fix(physics): restore retail path-6 collision response

This commit is contained in:
Erik 2026-07-31 13:44:55 +02:00
parent acec33eca8
commit 75b6f6b6c9
8 changed files with 523 additions and 223 deletions

View file

@ -231,16 +231,18 @@ public sealed class RetailEdgeResponseOrderingTests
}
[Fact]
public void MultiFrameSteepRoof_GraphAndFlatTraversalRemainExactAndDoNotWedge()
public void MultiFrameSteepRoof_PureVertical_GraphAndFlatSlideDownhillWithoutWedge()
{
TraceRun graph = RunSteepRoofTrace(preparedFlat: false);
TraceRun flat = RunSteepRoofTrace(preparedFlat: true);
TraceRun graph = RunSteepRoofTrace(preparedFlat: false, Vector2.Zero);
TraceRun flat = RunSteepRoofTrace(preparedFlat: true, Vector2.Zero);
AssertTraceParity(graph, flat);
Assert.Contains(graph.Frames, frame =>
frame.Result.Position.X < 0f
&& frame.Result.Position.Z <= BSPStepUpFixtures.SphereRadius + 0.05f);
Assert.Contains(graph.Frames, frame => frame.Result.InContact);
AssertNoLongFrozenStreak(graph.Frames, maximumTicks: 15);
Assert.True(graph.Frames[^1].Result.Position.X
< graph.Frames[0].Result.Position.X - 0.20f,
$"The vertical trace did not descend the roof: " +
$"{graph.Frames[0].Result.Position} -> {graph.Frames[^1].Result.Position}.");
Plane slope = BSPStepUpFixtures.SlopedUnwalkable().Resolved[
BSPStepUpFixtures.SlopedUnwalkable_SlopeId].Plane;
@ -272,6 +274,69 @@ public sealed class RetailEdgeResponseOrderingTests
}
}
[Theory]
[InlineData(-0.30f, 0f, "downhill")]
[InlineData( 0.30f, 0f, "uphill")]
[InlineData( 0f, 0.30f, "tangential")]
public void MultiFrameSteepRoof_DirectionalMotion_RemainsExactAndPreservesRetailResponse(
float velocityX,
float velocityY,
string direction)
{
Vector2 horizontalVelocity = new(velocityX, velocityY);
TraceRun graph = RunSteepRoofTrace(preparedFlat: false, horizontalVelocity);
TraceRun flat = RunSteepRoofTrace(preparedFlat: true, horizontalVelocity);
AssertTraceParity(graph, flat);
Assert.Contains(graph.Frames, frame => frame.Result.InContact);
AssertNoLongFrozenStreak(graph.Frames, maximumTicks: 15);
Vector3 first = graph.Frames[0].Result.Position;
Vector3 last = graph.Frames[^1].Result.Position;
Vector2 progress = new(last.X - first.X, last.Y - first.Y);
if (direction == "uphill")
{
int contactFrame = graph.Frames.FindIndex(frame => frame.Result.InContact);
Assert.True(contactFrame >= 0);
float peakAfterContact = graph.Frames
.GetRange(contactFrame, graph.Frames.Count - contactFrame)
.Max(frame => frame.Result.Position.Z);
Assert.True(peakAfterContact <= graph.Frames[contactFrame].Result.Position.Z + 0.001f,
$"The uphill trace launched/bounced from the roof: " +
$"contactZ={graph.Frames[contactFrame].Result.Position.Z}, peak={peakAfterContact}.");
}
else
{
Assert.True(Vector2.Dot(progress, Vector2.Normalize(horizontalVelocity)) > 0.10f,
$"The {direction} trace lost requested progress: {first} -> {last}.");
}
Plane slope = BSPStepUpFixtures.SlopedUnwalkable().Resolved[
BSPStepUpFixtures.SlopedUnwalkable_SlopeId].Plane;
float radius = BSPStepUpFixtures.SphereRadius;
for (int i = 0; i < graph.Frames.Count; i++)
{
Vector3 position = graph.Frames[i].Result.Position;
AssertFinite(position, $"steep-roof {direction} frame {i}");
if (i > 0)
{
float distance = Vector3.Distance(
graph.Frames[i - 1].Result.Position,
position);
Assert.InRange(distance, 0f, 1.1f);
}
if (position.X is >= 0f and <= 1f && MathF.Abs(position.Y) <= 1f)
{
Vector3 footCenter = position + new Vector3(0f, 0f, radius);
float signedDistance = Vector3.Dot(slope.Normal, footCenter) + slope.D;
Assert.True(signedDistance >= radius - 0.015f,
$"Steep-roof {direction} penetration at frame {i}: " +
$"distance={signedDistance}, radius={radius}, position={position}.");
}
}
}
[Fact]
public void MultiFrameFlatRoofLedge_GraphAndFlatTraversalRemainExactAndSlideAlongEdge()
{
@ -325,7 +390,7 @@ public sealed class RetailEdgeResponseOrderingTests
[Theory]
[InlineData(false)]
[InlineData(true)]
public void MultiFrameGroundedFloorWallSlide_FinalPlacementIsMandatoryAndGraphFlatExact(
public void MultiFrameGroundedFloorWallSlide_InwardTangentialMotionIsGraphFlatExact(
bool twoSpheres)
{
WallMaintenanceTrace graph = RunGroundedFloorWallSlide(
@ -391,14 +456,22 @@ public sealed class RetailEdgeResponseOrderingTests
new(-2f, 2f, 0f),
];
private static TraceRun RunSteepRoofTrace(bool preparedFlat)
private static TraceRun RunSteepRoofTrace(
bool preparedFlat,
Vector2 horizontalVelocity)
{
var fixture = BSPStepUpFixtures.SlopedUnwalkable();
PhysicsEngine engine = BuildCollisionEngine(fixture, preparedFlat, 0x0100E101u);
float radius = BSPStepUpFixtures.SphereRadius;
const float dt = 1f / 30f;
const float gravity = -9.8f;
var body = new PhysicsBody { TransientState = TransientStateFlags.Active };
var body = new PhysicsBody
{
Position = new Vector3(0.5f, 0f, 3f),
Orientation = Quaternion.Identity,
State = PhysicsStateFlags.Gravity | PhysicsStateFlags.ReportCollisions,
TransientState = TransientStateFlags.Active,
};
Vector3 position = new(0.5f, 0f, 3f);
float velocityZ = 0f;
var trace = new List<TraceFrame>(90);
@ -406,15 +479,19 @@ public sealed class RetailEdgeResponseOrderingTests
for (int tick = 0; tick < 90; tick++)
{
velocityZ += gravity * dt;
body.Velocity = new Vector3(
horizontalVelocity.X,
horizontalVelocity.Y,
velocityZ);
ResolveResult result = engine.ResolveWithTransition(
position,
position + new Vector3(0f, 0f, velocityZ * dt),
position + body.Velocity * dt,
Cell,
radius,
radius * 2f,
stepUpHeight: 0.30f,
stepDownHeight: 0.04f,
isOnGround: false,
isOnGround: body.OnWalkable,
body,
ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide,
movingEntityId: 0x01000000u);
@ -422,7 +499,13 @@ public sealed class RetailEdgeResponseOrderingTests
position = result.Position;
body.Position = position;
if (result.IsOnGround)
{
velocityZ = 0f;
body.Velocity = new Vector3(
horizontalVelocity.X,
horizontalVelocity.Y,
0f);
}
ApplyContactResult(body, result);
trace.Add(CaptureFrame(result, body));