diff --git a/src/AcDream.Core/Physics/PhysicsObjUpdate.cs b/src/AcDream.Core/Physics/PhysicsObjUpdate.cs
new file mode 100644
index 00000000..a1daa04d
--- /dev/null
+++ b/src/AcDream.Core/Physics/PhysicsObjUpdate.cs
@@ -0,0 +1,78 @@
+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 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.
+ }
+}
diff --git a/tests/AcDream.Core.Tests/Physics/HandleAllCollisionsTests.cs b/tests/AcDream.Core.Tests/Physics/HandleAllCollisionsTests.cs
new file mode 100644
index 00000000..9fdb8187
--- /dev/null
+++ b/tests/AcDream.Core.Tests/Physics/HandleAllCollisionsTests.cs
@@ -0,0 +1,111 @@
+using System.Numerics;
+using AcDream.Core.Physics;
+using Xunit;
+
+namespace AcDream.Core.Tests.Physics;
+
+///
+/// Conformance tests for — the port of
+/// retail CPhysicsObj::handle_all_collisions (0x00514780, pc:282647). This is the
+/// velocity "bleed on block" decision: reflect (fsf≤1) vs zero (fsf>1).
+///
+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);
+ }
+}