diff --git a/include/rendering/camera_controller.hpp b/include/rendering/camera_controller.hpp index 881e0aac..3d05c1b3 100644 --- a/include/rendering/camera_controller.hpp +++ b/include/rendering/camera_controller.hpp @@ -44,6 +44,15 @@ public: void reset(); void teleportTo(const glm::vec3& pos); void setOnlineMode(bool online) { onlineMode = online; } + + // Last known safe position (grounded, not falling) + bool hasLastSafePosition() const { return hasLastSafe_; } + const glm::vec3& getLastSafePosition() const { return lastSafePos_; } + float getContinuousFallTime() const { return continuousFallTime_; } + + // Auto-unstuck callback (triggered when falling too long) + using AutoUnstuckCallback = std::function; + void setAutoUnstuckCallback(AutoUnstuckCallback cb) { autoUnstuckCallback_ = std::move(cb); } void startIntroPan(float durationSec = 2.8f, float orbitDegrees = 140.0f); bool isIntroActive() const { return introActive; } bool isIdleOrbit() const { return idleOrbit_; } @@ -227,6 +236,23 @@ private: float idleTimer_ = 0.0f; bool idleOrbit_ = false; // true when current intro pan is an idle orbit (loops) static constexpr float IDLE_TIMEOUT = 120.0f; // 2 minutes + + // Last known safe position (saved periodically when grounded on real geometry) + bool hasLastSafe_ = false; + glm::vec3 lastSafePos_ = glm::vec3(0.0f); + float safePosSaveTimer_ = 0.0f; + bool hasRealGround_ = false; // True only when terrain/WMO/M2 floor is detected + static constexpr float SAFE_POS_SAVE_INTERVAL = 2.0f; // Save every 2 seconds + + // No-ground timer: after grace period, let the player fall instead of hovering + float noGroundTimer_ = 0.0f; + static constexpr float NO_GROUND_GRACE = 0.5f; // 500ms grace for terrain streaming + + // Continuous fall time (for auto-unstuck detection) + float continuousFallTime_ = 0.0f; + bool autoUnstuckFired_ = false; + AutoUnstuckCallback autoUnstuckCallback_; + static constexpr float AUTO_UNSTUCK_FALL_TIME = 5.0f; // 5 seconds of falling }; } // namespace rendering diff --git a/src/core/application.cpp b/src/core/application.cpp index 22591a15..ad25266a 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -593,50 +593,56 @@ void Application::setupUICallbacks() { loadOnlineWorldTerrain(mapId, x, y, z); }); - // Unstuck callback — move 5 units forward + // /unstuck — snap upward 10m to escape minor WMO cracks gameHandler->setUnstuckCallback([this]() { if (!renderer || !renderer->getCameraController()) return; auto* cc = renderer->getCameraController(); auto* ft = cc->getFollowTargetMutable(); if (!ft) return; - float yaw = cc->getYaw(); - ft->x += 5.0f * std::sin(yaw); - ft->y += 5.0f * std::cos(yaw); - cc->setDefaultSpawn(*ft, yaw, cc->getPitch()); - cc->reset(); + glm::vec3 pos = *ft; + pos.z += 10.0f; + cc->teleportTo(pos); }); - // Unstuck to nearest graveyard (WorldSafeLocs.dbc) + // /unstuckgy — snap upward 50m to clear all WMO geometry, gravity re-settles onto terrain gameHandler->setUnstuckGyCallback([this]() { - if (!renderer || !renderer->getCameraController() || !assetManager) return; + if (!renderer || !renderer->getCameraController()) return; auto* cc = renderer->getCameraController(); auto* ft = cc->getFollowTargetMutable(); if (!ft) return; - // Hardcoded safe locations per map (canonical WoW coords) - uint32_t mapId = gameHandler ? gameHandler->getCurrentMapId() : 0; - glm::vec3 safeCanonical; - switch (mapId) { - case 0: safeCanonical = glm::vec3(-8833.38f, 628.63f, 94.0f); break; // Stormwind Trade District - case 1: safeCanonical = glm::vec3(1629.36f, -4373.34f, 31.2f); break; // Orgrimmar - case 530: safeCanonical = glm::vec3(-3961.64f, -13931.2f, 100.6f); break; // Shattrath - case 571: safeCanonical = glm::vec3(5804.14f, 624.77f, 647.8f); break; // Dalaran - default: - LOG_WARNING("No hardcoded safe location for map ", mapId); - return; + // Try last safe position first (nearby, terrain already loaded) + if (cc->hasLastSafePosition()) { + glm::vec3 safePos = cc->getLastSafePosition(); + safePos.z += 5.0f; + cc->teleportTo(safePos); + LOG_INFO("Unstuck: teleported to last safe position"); + return; } - glm::vec3 safePos = core::coords::canonicalToRender(safeCanonical); - cc->setDefaultSpawn(safePos, cc->getYaw(), cc->getPitch()); - cc->teleportTo(safePos); + // No safe position — snap 50m upward to clear all WMO geometry + glm::vec3 pos = *ft; + pos.z += 50.0f; + cc->teleportTo(pos); + LOG_INFO("Unstuck: snapped 50m upward"); }); - // Bind point update (innkeeper) + // Auto-unstuck: falling for > 5 seconds = void fall, teleport to map entry + if (renderer->getCameraController()) { + renderer->getCameraController()->setAutoUnstuckCallback([this]() { + if (!renderer || !renderer->getCameraController()) return; + auto* cc = renderer->getCameraController(); + + // Last resort: teleport to map entry point (terrain guaranteed loaded here) + glm::vec3 spawnPos = cc->getDefaultPosition(); + spawnPos.z += 5.0f; + cc->teleportTo(spawnPos); + LOG_INFO("Auto-unstuck: teleported to map entry point"); + }); + } + + // Bind point update (innkeeper) — position stored in gameHandler->getHomeBind() gameHandler->setBindPointCallback([this](uint32_t mapId, float x, float y, float z) { - if (!renderer || !renderer->getCameraController()) return; - glm::vec3 canonical(x, y, z); - glm::vec3 renderPos = core::coords::canonicalToRender(canonical); - renderer->getCameraController()->setDefaultSpawn(renderPos, 0.0f, 15.0f); LOG_INFO("Bindpoint set: mapId=", mapId, " pos=(", x, ", ", y, ", ", z, ")"); }); diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 032e09d9..c770e2cf 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -4494,14 +4494,14 @@ void GameHandler::useItemById(uint32_t itemId) { void GameHandler::unstuck() { if (unstuckCallback_) { unstuckCallback_(); - addSystemChatMessage("Unstuck: moved 5 units forward."); + addSystemChatMessage("Unstuck: snapped upward. Use /unstuckgy for full teleport."); } } void GameHandler::unstuckGy() { if (unstuckGyCallback_) { unstuckGyCallback_(); - addSystemChatMessage("Unstuck: moved to nearest graveyard."); + addSystemChatMessage("Unstuck: teleported to safe location."); } } diff --git a/src/rendering/camera_controller.cpp b/src/rendering/camera_controller.cpp index c22505a1..0c436119 100644 --- a/src/rendering/camera_controller.cpp +++ b/src/rendering/camera_controller.cpp @@ -654,6 +654,8 @@ void CameraController::update(float deltaTime) { } if (groundH) { + hasRealGround_ = true; + noGroundTimer_ = 0.0f; float groundDiff = *groundH - lastGroundZ; if (groundDiff > 2.0f) { // Landing on a higher ledge - snap up @@ -674,16 +676,45 @@ void CameraController::update(float deltaTime) { grounded = false; } } else { - // No terrain found — hold at last known ground - targetPos.z = lastGroundZ; - verticalVelocity = 0.0f; - grounded = true; + hasRealGround_ = false; + noGroundTimer_ += deltaTime; + if (noGroundTimer_ < NO_GROUND_GRACE) { + // Brief grace period for terrain streaming — hold position + targetPos.z = lastGroundZ; + verticalVelocity = 0.0f; + grounded = true; + } else { + // No geometry found for too long — let player fall + grounded = false; + } } } // Update follow target position *followTarget = targetPos; + // --- Safe position caching + void fall detection --- + if (grounded && hasRealGround_ && !swimming && verticalVelocity >= 0.0f) { + // Player is safely on real geometry — save periodically + continuousFallTime_ = 0.0f; + autoUnstuckFired_ = false; + safePosSaveTimer_ += deltaTime; + if (safePosSaveTimer_ >= SAFE_POS_SAVE_INTERVAL) { + safePosSaveTimer_ = 0.0f; + lastSafePos_ = targetPos; + hasLastSafe_ = true; + } + } else if (!grounded && !swimming && !externalFollow_) { + // Falling (or standing on nothing past grace period) — accumulate fall time + continuousFallTime_ += deltaTime; + if (continuousFallTime_ >= AUTO_UNSTUCK_FALL_TIME && !autoUnstuckFired_) { + autoUnstuckFired_ = true; + if (autoUnstuckCallback_) { + autoUnstuckCallback_(); + } + } + } + // ===== WoW-style orbit camera ===== // Pivot point at upper chest/neck float mountedOffset = mounted_ ? mountHeightOffset_ : 0.0f; @@ -1100,6 +1131,8 @@ void CameraController::reset() { swimming = false; sitting = false; autoRunning = false; + noGroundTimer_ = 0.0f; + autoUnstuckFired_ = false; // Clear edge-state so movement packets can re-start cleanly after respawn. wasMovingForward = false; @@ -1293,7 +1326,11 @@ void CameraController::teleportTo(const glm::vec3& pos) { verticalVelocity = 0.0f; grounded = true; swimming = false; + sitting = false; lastGroundZ = pos.z; + noGroundTimer_ = 0.0f; // Reset grace period so terrain has time to stream + autoUnstuckFired_ = false; + continuousFallTime_ = 0.0f; if (thirdPerson && followTarget) { *followTarget = pos;