Fix stair approach fall-through and relax steep slope climbing

Relaxed walkable slope threshold from 0.40 to 0.35 (~70° max) for
steeper stair climbing. Tightened WMO floor cache above-tolerance
back to 0.25 units to prevent cached stair landing from overriding
approach floor. Added M2 floor preference for ship decks to prevent
falling through to water below.
This commit is contained in:
Kelsi 2026-02-08 20:31:00 -08:00
parent 58ec7693a1
commit dbadfb043b
3 changed files with 14 additions and 10 deletions

View file

@ -506,7 +506,7 @@ void CameraController::update(float deltaTime) {
groundH = selectReachableFloor(terrainH, wmoH, targetPos.z, stepUpBudget);
}
// 2. Multi-sample for M2 objects (rugs, planks, bridges) —
// 2. Multi-sample for M2 objects (rugs, planks, bridges, ships) —
// these are narrow and need offset probes to detect reliably.
if (m2Renderer) {
constexpr float FOOTPRINT = 0.4f;
@ -519,9 +519,13 @@ void CameraController::update(float deltaTime) {
for (const auto& o : offsets) {
auto m2H = m2Renderer->getFloorHeight(
targetPos.x + o.x, targetPos.y + o.y, m2ProbeZ);
if (m2H && *m2H <= targetPos.z + stepUpBudget &&
(!groundH || *m2H > *groundH)) {
groundH = m2H;
// Prefer M2 floors (ships, platforms) even if slightly lower than terrain
// to prevent falling through ship decks to water below
if (m2H && *m2H <= targetPos.z + stepUpBudget) {
if (!groundH || *m2H > *groundH ||
(*m2H >= targetPos.z - 0.5f && *groundH < targetPos.z - 1.0f)) {
groundH = m2H;
}
}
}
}

View file

@ -718,7 +718,7 @@ void M2ModelGPU::CollisionMesh::build() {
glm::vec3 normal = glm::cross(v1 - v0, v2 - v0);
float normalLen = glm::length(normal);
float absNz = (normalLen > 0.001f) ? std::abs(normal.z / normalLen) : 0.0f;
bool isFloor = (absNz >= 0.40f); // ~66° max slope (relaxed for steep stairs)
bool isFloor = (absNz >= 0.35f); // ~70° max slope (relaxed for steep stairs)
bool isWall = (absNz < 0.65f);
float triMinX = std::min({v0.x, v1.x, v2.x});
@ -2573,7 +2573,7 @@ std::optional<float> M2Renderer::getFloorHeight(float glX, float glY, float glZ)
if (localN.z < 0.0f) localN = -localN;
glm::vec3 worldN = glm::normalize(
glm::vec3(instance.modelMatrix * glm::vec4(localN, 0.0f)));
if (std::abs(worldN.z) < 0.40f) continue; // too steep (~66° max slope)
if (std::abs(worldN.z) < 0.35f) continue; // too steep (~70° max slope)
}
if (hitZ <= localPos.z + 3.0f && hitZ > bestHitZ) {

View file

@ -1607,7 +1607,7 @@ void WMORenderer::GroupResources::buildCollisionGrid() {
glm::vec3 normal = glm::cross(edge1, edge2);
float normalLen = glm::length(normal);
float absNz = (normalLen > 0.001f) ? std::abs(normal.z / normalLen) : 0.0f;
bool isFloor = (absNz >= 0.40f); // ~66° max slope (relaxed for steep stairs)
bool isFloor = (absNz >= 0.35f); // ~70° max slope (relaxed for steep stairs)
bool isWall = (absNz < 0.65f); // Matches walkable slope threshold
int cellMinX = std::max(0, static_cast<int>((triMinX - gridOrigin.x) * invCellW));
@ -1749,8 +1749,8 @@ std::optional<float> WMORenderer::getFloorHeight(float glX, float glY, float glZ
if (gridIt != precomputedFloorGrid.end()) {
float cachedHeight = gridIt->second;
// Only trust cache if it's basically at foot level.
// Prevent ledges/shoulder ramps from being treated as "floor".
constexpr float CACHE_ABOVE = 0.50f; // tune: 0.250.60 (increased to catch borderline floors)
// Reject cache if it's too high above us (prevents stair landing from overriding approach floor)
constexpr float CACHE_ABOVE = 0.25f; // tight above threshold (prevents stair landing cache hit)
constexpr float CACHE_BELOW = 4.0f; // keep generous below
if (cachedHeight <= glZ + CACHE_ABOVE && cachedHeight >= glZ - CACHE_BELOW) {
// Persistent cache doesn't store normal — report as flat
@ -2066,7 +2066,7 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to,
if (horizDist <= PLAYER_RADIUS) {
// Skip floor-like surfaces — grounding handles them, not wall collision
float absNz = std::abs(normal.z);
if (absNz >= 0.40f) continue;
if (absNz >= 0.35f) continue;
const float SKIN = 0.005f; // small separation so we don't re-collide immediately
// Stronger push when inside WMO for more responsive indoor collision