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. } }