using System.Numerics;
namespace AcDream.Core.Physics;
///
/// Verbatim port of the collision-response tail of retail
/// CPhysicsObj::UpdateObjectInternal: the velocity decision that
/// SetPositionInternal (0x00515330) drives through
/// handle_all_collisions (0x00514780, pc:282647). Kept as a pure function over a
/// + the resolve outcome so the whole decision is unit-testable
/// in Core, independent of the App per-frame loop. The transition INTERNALS
/// (ResolveWithTransition and below) are untouched.
///
public static class PhysicsObjUpdate
{
///
/// Retail SetPositionInternal walkability comparison at
/// 0x00515465-0x0051548E. Equality with FloorZ is walkable.
///
public static bool IsWalkableContact(bool inContact, Vector3 contactNormal)
=> inContact && contactNormal.Z >= PhysicsGlobals.FloorZ;
///
/// Applies the two independent transient facts written by retail
/// CPhysicsObj::SetPositionInternal at 0x00515430-0x0051549F.
/// Contact comes from contact-plane validity; OnWalkable additionally
/// requires a walkable plane normal. A steep contact therefore remains in
/// contact without becoming grounded.
///
public static void ApplySetPositionContact(
PhysicsBody body,
bool inContact,
bool onWalkable)
{
if (inContact)
body.TransientState |= TransientStateFlags.Contact;
else
body.TransientState &= ~TransientStateFlags.Contact;
if (inContact && onWalkable)
body.TransientState |= TransientStateFlags.OnWalkable;
else
body.TransientState &= ~TransientStateFlags.OnWalkable;
body.calc_acceleration();
}
///
/// Commits the contact/walkable/collision-response tail of retail
/// CPhysicsObj::SetPositionInternal (0x00515330) in its
/// original order. The pre-transition flags are explicit because a
/// deferred placement may temporarily park the body without contact while
/// still needing the source edge when its destination cell becomes ready.
///
///
/// Retail writes Contact and recalculates acceleration, calls
/// set_on_walkable (which invokes HitGround/LeaveGround), then calls
/// handle_all_collisions at 0x005154FE. Collision response
/// must therefore observe any velocity change made by the movement
/// callback; moving that callback after reflection changes the result.
///
public static bool CommitSetPositionTransition(
PhysicsBody body,
bool inContact,
bool onWalkable,
bool collisionNormalValid,
Vector3 collisionNormal,
bool previousContact,
bool previousOnWalkable,
Action? hitGround = null,
Action? leaveGround = null,
Func? isCurrent = null,
Func? isVelocityCurrent = null)
{
ArgumentNullException.ThrowIfNull(body);
// SetPositionInternal replaces Contact first but retains the source
// OnWalkable bit through its first calc_acceleration call. A deferred
// teleport may have parked the live body with both bits cleared, so
// restore the captured source bit explicitly before reproducing that
// ordering.
if (previousOnWalkable)
body.TransientState |= TransientStateFlags.OnWalkable;
else
body.TransientState &= ~TransientStateFlags.OnWalkable;
if (inContact)
body.TransientState |= TransientStateFlags.Contact;
else
body.TransientState &= ~TransientStateFlags.Contact;
body.calc_acceleration();
bool finalOnWalkable = inContact && onWalkable;
if (finalOnWalkable)
body.TransientState |= TransientStateFlags.OnWalkable;
else
body.TransientState &= ~TransientStateFlags.OnWalkable;
if (!previousOnWalkable && finalOnWalkable)
{
hitGround?.Invoke();
if (isCurrent?.Invoke() == false)
return false;
}
else if (previousOnWalkable && !finalOnWalkable)
{
leaveGround?.Invoke();
if (isCurrent?.Invoke() == false)
return false;
}
body.calc_acceleration();
// Position, Vector, and Movement are independently timestamped but
// can all install m_velocityVector. If a later one arrived from a
// callback above, retain its vector and finish the non-overlapping
// contact/pose commit without applying this older collision response.
if (isVelocityCurrent?.Invoke() == false)
return isCurrent?.Invoke() ?? true;
HandleAllCollisions(
body,
collisionNormalValid,
collisionNormal,
previousContact,
previousOnWalkable,
finalOnWalkable);
return isCurrent?.Invoke() ?? true;
}
///
/// retail handle_all_collisions (0x00514780). Reflects or zeros the body's
/// (retail m_velocityVector) based on
/// :
///
/// - fsf ≤ 1 → reflect the into-surface component
/// (v += -(v·n)(elasticity+1)·n, pc:282712) when we should reflect and a
/// collision normal is valid; an INELASTIC mover zeros instead (pc:282720).
/// - fsf > 1 → v = 0 entirely (pc:282729) — the "bleed on block" that
/// lets gravity resume so a blocked jump falls/glides off (fixes the #182
/// airborne-stuck wedge).
///
/// The Stationary* transient-bit round-trip (pc:282737-758) is owned by the Core resolve
/// writeback (PhysicsEngine.ResolveWithTransition), not re-encoded here.
///
/// The mover.
/// Was a wall/creature collision normal recorded this resolve.
/// Outward collision normal (points away from the surface).
/// Whether the body had Contact BEFORE this resolve committed
/// (retail arg3 — reserved for environment-collision reporting; unused today).
/// Whether the body was OnWalkable before this resolve (retail arg4).
/// Whether the body is OnWalkable after this resolve.
public static void HandleAllCollisions(
PhysicsBody body,
bool collisionNormalValid, Vector3 collisionNormal,
bool prevContact, bool prevOnWalkable, bool nowOnWalkable)
{
// var_10_1 (pc:282653-282657): reflect UNLESS the mover stays on walkable ground
// (and is not sledding). This restores retail's broader rule — the AD-25 airborne-only
// suppression is retired: the landing-snap fragility it guarded is gone (landing state
// is now owned by the SetPositionInternal-derived contact flags, not a Velocity.Z<=0
// gate). A grounded corridor wall-slide keeps its tangential velocity (should_reflect
// false), exactly as retail.
bool sledding = body.State.HasFlag(PhysicsStateFlags.Sledding);
bool shouldReflect = !(prevOnWalkable && nowOnWalkable && !sledding);
if (body.FramesStationaryFall <= 1)
{
if (shouldReflect && collisionNormalValid)
{
if (body.State.HasFlag(PhysicsStateFlags.Inelastic))
{
body.Velocity = Vector3.Zero; // pc:282720-282722
}
else
{
float dot = Vector3.Dot(body.Velocity, collisionNormal);
if (dot < 0f) // moving INTO the surface
{
float k = -(dot * (body.Elasticity + 1f)); // pc:282712
body.Velocity += collisionNormal * k;
}
}
}
}
else
{
body.Velocity = Vector3.Zero; // fsf>1 → THE BLEED (pc:282729)
}
_ = prevContact; // retail report_environment_collision(arg3) — weenie collision events, later.
}
}