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>
110 lines
5.1 KiB
C#
110 lines
5.1 KiB
C#
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}");
|
|
}
|
|
}
|