Ok, I have come up with an initial implementation using a mix of techniques:
1. Loop through all func_ladder entities using UTIL_FindEntityByClassname
2. Define a line going from the ladder entity's bounding box min and max (pev->absmin, pev->absmax)
3. Calculate the distance of the player's origin from that line (code for this below) and pick whichever ladder produces the lowest distance
4. Find the midpoint between the ladder's bounding box min and max, and then use UTIL_TRACEHULL from the player's origin to that midpoint to get the impact normal, which will be your ladder's surface normal. We use TRACEHULL rather than TRACELINE in case the ladder is a func_illusionary
There would be an issue with very fat ladder brushes since the distance from that line would be quite high, but these are generally very rare and it's unlikely to have a really fat ladder brush close to a thin ladder brush, so probably not an edge case worth investigating.
Vector UTIL_ClosestPointOnLine(Vector lineFrom, Vector lineTo, Vector point) {
Vector vVector1 = point - lineFrom;
Vector vVector2 = (lineTo - lineFrom);
UTIL_NormalizeVector(&vVector2);
float d = vDist3D(lineFrom, lineTo);
float t = UTIL_GetDotProduct(vVector2, vVector1);
if (t <= 0)
return lineFrom;
if (t >= d)
return lineTo;
Vector vVector3 = vVector2 * t;
Vector vClosestPoint = lineFrom + vVector3;
return vClosestPoint;
}
float UTIL_DistanceFromLine3D(Vector lineFrom, Vector lineTo, Vector point) {
Vector nearestToLine = UTIL_ClosestPointOnLine(lineFrom, lineTo, point);
return vDist3D(point, nearestToLine);
}