feat(#182): route player collision response through the ported UpdateObjectInternal chain (Slice 2b/2c)
Replaces PlayerMovementController's ad-hoc airborne-only reflect + Velocity.Z landing snap with the verbatim retail chain: cached_velocity = (resolved-old)/dt (separate reporting value), SetPositionInternal contact determination (kept Velocity.Z<=0 gate for acdream's always-step-down resolver), then handle_all_collisions (fsf<=1 reflect / fsf>1 zero — the airborne-stuck bleed). Contact is committed BEFORE the reflect so the landing gate isn't defeated by a reflected +Z; that ordering + the ungated small-velocity-zero (Slice 1a) retire AD-25's micro-bounce spiral. Load-bearing: handle_all_collisions + cached_velocity are gated on candidateMoved (retail UpdateObjectInternal pc:283657 only reaches SetPositionInternal when the integrated candidate moved off m_position). After fsf>1 zeros a blocked jump, the next frame integrates zero motion (velMag2==0), so the candidate hasn't moved — skipping the response that frame lets gravity rebuild the velocity instead of re-zeroing it and re-wedging. End-to-end Core test (Issue182CrowdJumpTests): a jump blocked by an overhead creature bleeds its +12 up-velocity to ~0 within a couple frames (fsf>1) and the body grounds on the manufactured plane instead of hanging with persistent +12. Core 2617 / App 741 green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
78df1370b6
commit
54d5622960
2 changed files with 177 additions and 147 deletions
110
tests/AcDream.Core.Tests/Physics/Issue182CrowdJumpTests.cs
Normal file
110
tests/AcDream.Core.Tests/Physics/Issue182CrowdJumpTests.cs
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
using System;
|
||||
using System.Numerics;
|
||||
using AcDream.Core.Physics;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace AcDream.Core.Tests.Physics;
|
||||
|
||||
/// <summary>
|
||||
/// End-to-end (Core) proof of the #182 airborne-stuck fix: a jump velocity blocked by a
|
||||
/// creature BLEEDS to zero within a few frames so gravity can resume — the player falls /
|
||||
/// glides off instead of hanging in the falling animation. Chains the same steps the
|
||||
/// PlayerMovementController per-frame loop runs, minus the App layer:
|
||||
/// integrate (UpdatePhysicsInternal + gravity) → ResolveWithTransition → apply →
|
||||
/// handle_all_collisions (PhysicsObjUpdate).
|
||||
/// Before the rebuild the velocity persisted (only an airborne-only reflect that barely
|
||||
/// touched +Z against a near-horizontal normal); now fs>1 zeros it.
|
||||
/// </summary>
|
||||
public class Issue182CrowdJumpTests
|
||||
{
|
||||
private readonly ITestOutputHelper _out;
|
||||
public Issue182CrowdJumpTests(ITestOutputHelper output) => _out = output;
|
||||
|
||||
private const uint Lb = 0xA9B40000u;
|
||||
private const uint Cell = Lb | 0x0001u;
|
||||
private const float R = 0.48f, H = 1.835f, StepUp = 0.60f, StepDown = 0.04f;
|
||||
private const float Dt = 1f / 30f; // one physics quantum
|
||||
|
||||
private static PhysicsEngine BuildEngine()
|
||||
{
|
||||
var engine = new PhysicsEngine { DataCache = new PhysicsDataCache() };
|
||||
engine.AddLandblock(Lb, new TerrainSurface(new byte[81], new float[256]),
|
||||
Array.Empty<CellSurface>(), Array.Empty<PortalPlane>(), 0f, 0f);
|
||||
return engine;
|
||||
}
|
||||
|
||||
private static void RegisterCreatureAt(PhysicsEngine e, uint id, Vector3 c)
|
||||
=> e.ShadowObjects.Register(id, 0u, c, Quaternion.Identity, R,
|
||||
0f, 0f, Lb, ShadowCollisionType.Sphere, 0f, 1f, 0u,
|
||||
EntityCollisionFlags.IsCreature, isStatic: false);
|
||||
|
||||
[Fact]
|
||||
public void BlockedJump_VelocityBleedsToZero_ThenGravityResumes()
|
||||
{
|
||||
var engine = BuildEngine();
|
||||
RegisterCreatureAt(engine, 0xC0B0u, new Vector3(12f, 10f, 4.5f)); // creature overhead
|
||||
|
||||
var body = new PhysicsBody
|
||||
{
|
||||
Position = new Vector3(12f, 10f, 3.0f),
|
||||
Orientation = Quaternion.Identity,
|
||||
State = PhysicsStateFlags.Gravity | PhysicsStateFlags.ReportCollisions,
|
||||
TransientState = TransientStateFlags.None, // airborne
|
||||
Velocity = new Vector3(0f, 0f, 12f), // a jump: moving straight up
|
||||
};
|
||||
uint cell = Cell;
|
||||
|
||||
bool bled = false;
|
||||
for (int frame = 0; frame < 20; frame++)
|
||||
{
|
||||
// 1) integrate velocity + gravity into a candidate (mirrors the controller's §4).
|
||||
var pre = body.Position;
|
||||
body.calc_acceleration();
|
||||
body.UpdatePhysicsInternal(Dt);
|
||||
var post = body.Position;
|
||||
bool candidateMoved = post != pre; // retail candidate != m_position gate
|
||||
|
||||
// 2) resolve the candidate against the crowd.
|
||||
var r = engine.ResolveWithTransition(pre, post, cell, R, H, StepUp, StepDown,
|
||||
isOnGround: false, body: body,
|
||||
moverFlags: ObjectInfoState.IsPlayer | ObjectInfoState.EdgeSlide);
|
||||
|
||||
// 3) apply + contact determination (mirrors the controller's SetPositionInternal).
|
||||
bool prevOnWalkable = body.OnWalkable;
|
||||
body.Position = r.Position;
|
||||
cell = r.CellId;
|
||||
if (r.IsOnGround && body.Velocity.Z <= 0f)
|
||||
{
|
||||
body.TransientState |= TransientStateFlags.Contact | TransientStateFlags.OnWalkable;
|
||||
body.calc_acceleration();
|
||||
if (body.Velocity.Z < 0f) body.Velocity = new Vector3(body.Velocity.X, body.Velocity.Y, 0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
body.TransientState &= ~(TransientStateFlags.Contact | TransientStateFlags.OnWalkable);
|
||||
body.calc_acceleration();
|
||||
}
|
||||
|
||||
// 4) handle_all_collisions: reflect (fsf<=1) or zero (fsf>1) — but ONLY when the
|
||||
// candidate moved (retail skips SetPositionInternal otherwise), so gravity can
|
||||
// rebuild the velocity after the bleed instead of it being re-zeroed each frame.
|
||||
if (candidateMoved)
|
||||
PhysicsObjUpdate.HandleAllCollisions(body,
|
||||
r.CollisionNormalValid, r.CollisionNormal,
|
||||
prevContact: false, prevOnWalkable, nowOnWalkable: body.OnWalkable);
|
||||
|
||||
_out.WriteLine($"frame{frame,2}: z={body.Position.Z:F3} vz={body.Velocity.Z:F3} " +
|
||||
$"fsf={body.FramesStationaryFall}");
|
||||
|
||||
// The upward jump velocity must collapse to ~0 (or reverse to falling) within a few
|
||||
// frames of being blocked — the bleed. Before the fix it persisted near +12.
|
||||
if (frame >= 1 && frame <= 6 && body.Velocity.Z <= 0.5f)
|
||||
bled = true;
|
||||
}
|
||||
|
||||
Assert.True(bled, "the blocked jump velocity must bleed to ~0 within a few frames (fsf>1 → v=0)");
|
||||
// After bleeding, gravity has taken over — the body is no longer being shoved upward.
|
||||
Assert.True(body.Velocity.Z <= 0.5f, $"velocity should not persist upward; got vz={body.Velocity.Z:F3}");
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue