From 8014dde29b74b8dc8565557be3e350b5c8bb31f7 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 6 Mar 2026 20:00:27 -0800 Subject: [PATCH] Improve WMO wall collision, unstuck, interior zoom, and chat focus - Stronger wall collision push (0.35/0.15) and swept push (0.45/0.25) for interior/exterior WMOs to reduce clipping through tunnel walls - Use all triangles (not just pre-classified walls) for collision checks - Allow invisible collidable triangles (MOPY 0x01 without 0x20) to block - Pass insideWMO flag to all collision callers, match swim sweep to ground - Widen swept hit detection radius from 0.15 to 0.25 - Restrict camera zoom to 12 units inside WMO interiors - Fix /unstuck launching player above WMOs: remove +20 fallback, use gravity when no floor found - Slash and Enter keys always focus chat unless already typing --- include/rendering/camera_controller.hpp | 2 ++ src/core/application.cpp | 8 ++++++-- src/rendering/camera_controller.cpp | 17 +++++++++++------ src/rendering/wmo_renderer.cpp | 25 +++++++++++++------------ src/ui/game_screen.cpp | 8 ++++---- 5 files changed, 36 insertions(+), 24 deletions(-) diff --git a/include/rendering/camera_controller.hpp b/include/rendering/camera_controller.hpp index 3431ce55..06ba2578 100644 --- a/include/rendering/camera_controller.hpp +++ b/include/rendering/camera_controller.hpp @@ -81,6 +81,7 @@ public: bool isSitting() const { return sitting; } bool isSwimming() const { return swimming; } bool isInsideWMO() const { return cachedInsideWMO; } + void setGrounded(bool g) { grounded = g; } bool isOnTaxi() const { return externalFollow_; } const glm::vec3* getFollowTarget() const { return followTarget; } glm::vec3* getFollowTargetMutable() { return followTarget; } @@ -141,6 +142,7 @@ private: static constexpr float MIN_DISTANCE = 0.5f; // Minimum zoom (first-person threshold) static constexpr float MAX_DISTANCE_NORMAL = 22.0f; // Default max zoom out static constexpr float MAX_DISTANCE_EXTENDED = 50.0f; // Extended max zoom out + static constexpr float MAX_DISTANCE_INTERIOR = 12.0f; // Max zoom inside WMOs bool extendedZoom_ = false; static constexpr float ZOOM_SMOOTH_SPEED = 15.0f; // How fast zoom eases static constexpr float CAM_SMOOTH_SPEED = 20.0f; // How fast camera position smooths diff --git a/src/core/application.cpp b/src/core/application.cpp index d169b156..d5da23ff 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -1667,13 +1667,17 @@ void Application::setupUICallbacks() { } // Sample floor at the DESTINATION position (after nudge). + // Pick the highest floor so we snap up to WMO floors when fallen below. + bool foundFloor = false; if (auto floor = sampleBestFloorAt(pos.x, pos.y, pos.z + 60.0f)) { pos.z = *floor + 0.2f; - } else { - pos.z += 20.0f; + foundFloor = true; } cc->teleportTo(pos); + if (!foundFloor) { + cc->setGrounded(false); // Let gravity pull player down to a surface + } syncTeleportedPositionToServer(pos); forceServerTeleportCommand(pos); clearStuckMovement(); diff --git a/src/rendering/camera_controller.cpp b/src/rendering/camera_controller.cpp index e184f842..392ef71b 100644 --- a/src/rendering/camera_controller.cpp +++ b/src/rendering/camera_controller.cpp @@ -626,7 +626,8 @@ void CameraController::update(float deltaTime) { glm::vec3 stepPos = swimFrom; if (swimMoveDist > 0.01f) { - int swimSteps = std::max(1, std::min(3, static_cast(std::ceil(swimMoveDist / 0.65f)))); + float swimStepSize = cachedInsideWMO ? 0.20f : 0.35f; + int swimSteps = std::max(1, std::min(8, static_cast(std::ceil(swimMoveDist / swimStepSize)))); glm::vec3 stepDelta = (swimTo - swimFrom) / static_cast(swimSteps); for (int i = 0; i < swimSteps; i++) { @@ -634,7 +635,7 @@ void CameraController::update(float deltaTime) { if (wmoRenderer) { glm::vec3 adjusted; - if (wmoRenderer->checkWallCollision(stepPos, candidate, adjusted)) { + if (wmoRenderer->checkWallCollision(stepPos, candidate, adjusted, cachedInsideWMO)) { candidate.x = adjusted.x; candidate.y = adjusted.y; candidate.z = std::max(candidate.z, adjusted.z); @@ -1274,8 +1275,10 @@ void CameraController::update(float deltaTime) { lastInsideWMOCheckPos = targetPos; } - // Do not clamp zoom target by ceiling checks. First-person should always - // be reachable; occlusion handling below will resolve camera placement safely. + // Smoothly pull camera in when entering WMO interiors + if (cachedInsideWMO && userTargetDistance > MAX_DISTANCE_INTERIOR) { + userTargetDistance = MAX_DISTANCE_INTERIOR; + } } // ===== Camera collision (sphere sweep approximation) ===== @@ -1499,14 +1502,15 @@ void CameraController::update(float deltaTime) { float moveDist = glm::length(desiredFeet - startFeet); if (moveDist > 0.01f) { - int sweepSteps = std::max(1, std::min(3, static_cast(std::ceil(moveDist / 0.65f)))); + float stepSize = cachedInsideWMO ? 0.20f : 0.35f; + int sweepSteps = std::max(1, std::min(8, static_cast(std::ceil(moveDist / stepSize)))); glm::vec3 stepPos = startFeet; glm::vec3 stepDelta = (desiredFeet - startFeet) / static_cast(sweepSteps); for (int i = 0; i < sweepSteps; i++) { glm::vec3 candidate = stepPos + stepDelta; glm::vec3 adjusted; - if (wmoRenderer->checkWallCollision(stepPos, candidate, adjusted)) { + if (wmoRenderer->checkWallCollision(stepPos, candidate, adjusted, cachedInsideWMO)) { candidate.x = adjusted.x; candidate.y = adjusted.y; candidate.z = std::max(candidate.z, adjusted.z); @@ -1985,6 +1989,7 @@ void CameraController::processMouseWheel(float delta) { float zoomSpeed = glm::max(userTargetDistance * 0.15f, 0.3f); userTargetDistance -= delta * zoomSpeed; float maxDist = extendedZoom_ ? MAX_DISTANCE_EXTENDED : MAX_DISTANCE_NORMAL; + if (cachedInsideWMO) maxDist = std::min(maxDist, MAX_DISTANCE_INTERIOR); userTargetDistance = glm::clamp(userTargetDistance, MIN_DISTANCE, maxDist); } diff --git a/src/rendering/wmo_renderer.cpp b/src/rendering/wmo_renderer.cpp index 5c8be756..6fe36a41 100644 --- a/src/rendering/wmo_renderer.cpp +++ b/src/rendering/wmo_renderer.cpp @@ -3095,7 +3095,7 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to, float rangeMinY = std::min(localFrom.y, localTo.y) - PLAYER_RADIUS - 1.5f; float rangeMaxX = std::max(localFrom.x, localTo.x) + PLAYER_RADIUS + 1.5f; float rangeMaxY = std::max(localFrom.y, localTo.y) + PLAYER_RADIUS + 1.5f; - group.getWallTrianglesInRange(rangeMinX, rangeMinY, rangeMaxX, rangeMaxY, triScratch_); + group.getTrianglesInRange(rangeMinX, rangeMinY, rangeMaxX, rangeMaxY, triScratch_); for (uint32_t triStart : triScratch_) { // Use pre-computed Z bounds for fast vertical reject @@ -3113,17 +3113,18 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to, if (triHeight < 1.0f && tb.maxZ <= localFeetZ + 1.2f) continue; // Use MOPY flags to filter wall collision. - // Only RENDERED triangles (flag 0x20) with collision intent (0x01) - // should block the player. Skip invisible collision hulls (0x08/0x48) - // and non-collidable render-only geometry. + // Collidable triangles (flag 0x01) block the player — including + // invisible collision walls (0x01 without 0x20) used in tunnels. + // Skip detail/decorative geometry (0x04) and render-only surfaces. uint32_t triIdx = triStart / 3; if (!group.triMopyFlags.empty() && triIdx < group.triMopyFlags.size()) { uint8_t mopy = group.triMopyFlags[triIdx]; - // Must be rendered (0x20) AND have base collision flag (0x01) - bool rendered = (mopy & 0x20) != 0; - bool collidable = (mopy & 0x01) != 0; - if (mopy != 0 && !(rendered && collidable)) { - continue; + if (mopy != 0) { + bool collidable = (mopy & 0x01) != 0; + bool detail = (mopy & 0x04) != 0; + if (!collidable || detail) { + continue; + } } } @@ -3149,13 +3150,13 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to, glm::vec3 hitPoint = localFrom + (localTo - localFrom) * tHit; glm::vec3 hitClosest = closestPointOnTriangle(hitPoint, v0, v1, v2); float hitErrSq = glm::dot(hitClosest - hitPoint, hitClosest - hitPoint); - if (hitErrSq <= 0.15f * 0.15f) { + if (hitErrSq <= 0.25f * 0.25f) { float side = fromDist > 0.0f ? 1.0f : -1.0f; glm::vec3 safeLocal = hitPoint + normal * side * (PLAYER_RADIUS + 0.05f); glm::vec3 pushLocal(safeLocal.x - localTo.x, safeLocal.y - localTo.y, 0.0f); // Cap swept pushback so walls don't shove the player violently float pushLen = glm::length(glm::vec2(pushLocal.x, pushLocal.y)); - const float MAX_SWEPT_PUSH = 0.15f; + const float MAX_SWEPT_PUSH = insideWMO ? 0.45f : 0.25f; if (pushLen > MAX_SWEPT_PUSH) { float scale = MAX_SWEPT_PUSH / pushLen; pushLocal.x *= scale; @@ -3185,7 +3186,7 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to, const float SKIN = 0.005f; // small separation so we don't re-collide immediately // Stronger push when inside WMO for more responsive indoor collision - const float MAX_PUSH = insideWMO ? 0.12f : 0.08f; + const float MAX_PUSH = insideWMO ? 0.35f : 0.15f; float penetration = (PLAYER_RADIUS - horizDist); float pushDist = glm::clamp(penetration + SKIN, 0.0f, MAX_PUSH); glm::vec2 pushDir2; diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 98c2f4fb..1d02d795 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -1367,16 +1367,16 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) { } - // Slash key: focus chat input - if (!io.WantCaptureKeyboard && input.isKeyJustPressed(SDL_SCANCODE_SLASH)) { + // Slash key: focus chat input — always works unless already typing in chat + if (!chatInputActive && input.isKeyJustPressed(SDL_SCANCODE_SLASH)) { refocusChatInput = true; chatInputBuffer[0] = '/'; chatInputBuffer[1] = '\0'; chatInputMoveCursorToEnd = true; } - // Enter key: focus chat input (empty) - if (!io.WantCaptureKeyboard && input.isKeyJustPressed(SDL_SCANCODE_RETURN)) { + // Enter key: focus chat input (empty) — always works unless already typing + if (!chatInputActive && input.isKeyJustPressed(SDL_SCANCODE_RETURN)) { refocusChatInput = true; }