feat(#182): port handle_all_collisions as a pure Core unit (Slice 2a)
PhysicsObjUpdate.HandleAllCollisions — retail CPhysicsObj::handle_all_collisions (0x00514780, pc:282647): the velocity 'bleed on block' decision. fsf<=1 → reflect the into-surface component (v += -(v·n)(elasticity+1)·n) unless staying-on-walkable (retail's should_reflect guard, restoring the broader rule AD-25 suppressed); INELASTIC zeros instead; fsf>1 → v=0 entirely (the airborne-stuck fix). Bit round-trip owned by the Core resolve writeback (Slice 1), not re-encoded here. 7 conformance tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
6c3cd96b7a
commit
78df1370b6
2 changed files with 189 additions and 0 deletions
111
tests/AcDream.Core.Tests/Physics/HandleAllCollisionsTests.cs
Normal file
111
tests/AcDream.Core.Tests/Physics/HandleAllCollisionsTests.cs
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
using System.Numerics;
|
||||
using AcDream.Core.Physics;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.Core.Tests.Physics;
|
||||
|
||||
/// <summary>
|
||||
/// Conformance tests for <see cref="PhysicsObjUpdate.HandleAllCollisions"/> — the port of
|
||||
/// retail <c>CPhysicsObj::handle_all_collisions</c> (0x00514780, pc:282647). This is the
|
||||
/// velocity "bleed on block" decision: reflect (fsf≤1) vs zero (fsf>1).
|
||||
/// </summary>
|
||||
public class HandleAllCollisionsTests
|
||||
{
|
||||
private static PhysicsBody Airborne(Vector3 v, int fsf = 0) => new PhysicsBody
|
||||
{
|
||||
State = PhysicsStateFlags.Gravity | PhysicsStateFlags.ReportCollisions,
|
||||
TransientState = TransientStateFlags.None,
|
||||
Velocity = v,
|
||||
FramesStationaryFall = fsf,
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void Fsf0_AirborneWallHit_ReflectsIntoWallComponent()
|
||||
{
|
||||
var b = Airborne(new Vector3(3f, 0f, 0f)); // moving +X into a wall whose outward normal is -X
|
||||
var n = new Vector3(-1f, 0f, 0f);
|
||||
PhysicsObjUpdate.HandleAllCollisions(b,
|
||||
collisionNormalValid: true, collisionNormal: n,
|
||||
prevContact: false, prevOnWalkable: false, nowOnWalkable: false);
|
||||
// dot = 3*-1 = -3 < 0 → k = -(-3*(0.05+1)) = 3.15 → v += (-1,0,0)*3.15 → x = 3 - 3.15 = -0.15
|
||||
Assert.Equal(-0.15f, b.Velocity.X, precision: 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fsf0_MovingAwayFromSurface_DoesNotReflect()
|
||||
{
|
||||
var b = Airborne(new Vector3(0f, 0f, 2f)); // moving up, normal also up (already separating)
|
||||
var n = new Vector3(0f, 0f, 1f);
|
||||
PhysicsObjUpdate.HandleAllCollisions(b,
|
||||
collisionNormalValid: true, collisionNormal: n,
|
||||
prevContact: false, prevOnWalkable: false, nowOnWalkable: false);
|
||||
Assert.Equal(2f, b.Velocity.Z); // dot = +2 >= 0 → no reflection
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fsf2_ZeroesVelocity_TheAirborneStuckBleed()
|
||||
{
|
||||
// The #182 case: a straight-up jump blocked by a near-horizontal creature normal.
|
||||
// At fsf>1 the whole velocity is zeroed so gravity resumes → the player falls/glides off.
|
||||
var b = Airborne(new Vector3(0f, 0f, 18f), fsf: 2);
|
||||
var n = new Vector3(-0.96f, -0.25f, -0.15f);
|
||||
PhysicsObjUpdate.HandleAllCollisions(b,
|
||||
collisionNormalValid: true, collisionNormal: n,
|
||||
prevContact: false, prevOnWalkable: false, nowOnWalkable: false);
|
||||
Assert.Equal(Vector3.Zero, b.Velocity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fsf3_ZeroesVelocity_EvenWithoutCollisionNormal()
|
||||
{
|
||||
var b = Airborne(new Vector3(1f, 2f, 18f), fsf: 3);
|
||||
PhysicsObjUpdate.HandleAllCollisions(b,
|
||||
collisionNormalValid: false, collisionNormal: default,
|
||||
prevContact: false, prevOnWalkable: false, nowOnWalkable: false);
|
||||
Assert.Equal(Vector3.Zero, b.Velocity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StayingOnWalkable_DoesNotReflect_CorridorWallSlidePreserved()
|
||||
{
|
||||
// Grounded before AND after → should_reflect is false → the tangential wall-slide
|
||||
// velocity is preserved (retail's rule; the corridor shuffle, not a sticky bounce).
|
||||
var b = new PhysicsBody
|
||||
{
|
||||
Velocity = new Vector3(3f, 0f, 0f),
|
||||
TransientState = TransientStateFlags.Contact | TransientStateFlags.OnWalkable,
|
||||
FramesStationaryFall = 0,
|
||||
};
|
||||
var n = new Vector3(-1f, 0f, 0f);
|
||||
PhysicsObjUpdate.HandleAllCollisions(b,
|
||||
collisionNormalValid: true, collisionNormal: n,
|
||||
prevContact: true, prevOnWalkable: true, nowOnWalkable: true);
|
||||
Assert.Equal(3f, b.Velocity.X);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Inelastic_ZeroesInsteadOfReflecting()
|
||||
{
|
||||
var b = Airborne(new Vector3(3f, 0f, 0f));
|
||||
b.State |= PhysicsStateFlags.Inelastic; // spell projectile / missile
|
||||
var n = new Vector3(-1f, 0f, 0f);
|
||||
PhysicsObjUpdate.HandleAllCollisions(b,
|
||||
collisionNormalValid: true, collisionNormal: n,
|
||||
prevContact: false, prevOnWalkable: false, nowOnWalkable: false);
|
||||
Assert.Equal(Vector3.Zero, b.Velocity);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LandingReflects_RetailRuleRestored_NotSuppressed()
|
||||
{
|
||||
// prev airborne → now grounded (a landing). Retail reflects here too (AD-25 retired);
|
||||
// at elasticity 0.05 the effect is a tiny, imperceptible deflection.
|
||||
var b = Airborne(new Vector3(0f, 0f, -5f)); // falling
|
||||
var n = new Vector3(0f, 0f, 1f); // floor normal up
|
||||
PhysicsObjUpdate.HandleAllCollisions(b,
|
||||
collisionNormalValid: true, collisionNormal: n,
|
||||
prevContact: false, prevOnWalkable: false, nowOnWalkable: true);
|
||||
// dot = -5 < 0 → k = -(-5*1.05) = 5.25 → v.z = -5 + 5.25 = 0.25 (tiny bounce)
|
||||
Assert.Equal(0.25f, b.Velocity.Z, precision: 3);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue