From 38210ec18602620fcd1f201d3aa11a3e7c802a64 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Mar 2026 09:02:20 -0700 Subject: [PATCH] fix(gameplay): keep timeout animation stable on repeated presses and harden M2 elevator sync --- src/core/application.cpp | 55 +++++++++++++++++++------- src/game/game_handler.cpp | 72 ++++++++++++++++++++++++++++------ src/game/transport_manager.cpp | 18 ++++++--- 3 files changed, 111 insertions(+), 34 deletions(-) diff --git a/src/core/application.cpp b/src/core/application.cpp index 9d0a30e1..8cea388a 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -504,16 +504,6 @@ void Application::run() { LOG_INFO("Shadows: ", enabled ? "ON" : "OFF"); } } - // F7: Test level-up effect (ignore key repeat) - else if (event.key.keysym.scancode == SDL_SCANCODE_F7 && event.key.repeat == 0) { - if (renderer) { - renderer->triggerLevelUpEffect(renderer->getCharacterPosition()); - LOG_INFO("Triggered test level-up effect"); - } - if (uiManager) { - uiManager->getGameScreen().triggerDing(99); - } - } // F8: Debug WMO floor at current position else if (event.key.keysym.scancode == SDL_SCANCODE_F8 && event.key.repeat == 0) { if (renderer && renderer->getWMORenderer()) { @@ -1376,6 +1366,24 @@ void Application::update(float deltaTime) { } lastTransportCanonical = tr->position; lastTransportGuid = gameHandler->getPlayerTransportGuid(); + + // Keep passenger locked to elevator vertical motion while grounded. + // Without this, floor clamping can hold world-Z static unless the + // player is jumping, which makes lifts appear to not move vertically. + glm::vec3 tentativeCanonical = core::coords::renderToCanonical(renderPos); + glm::vec3 localOffset = gameHandler->getPlayerTransportOffset(); + localOffset.x = tentativeCanonical.x - tr->position.x; + localOffset.y = tentativeCanonical.y - tr->position.y; + if (renderer->getCameraController() && + !renderer->getCameraController()->isGrounded()) { + // While airborne (jump/fall), allow local Z offset to change. + localOffset.z = tentativeCanonical.z - tr->position.z; + } + gameHandler->setPlayerTransportOffset(localOffset); + + glm::vec3 lockedCanonical = tr->position + localOffset; + renderPos = core::coords::canonicalToRender(lockedCanonical); + renderer->getCharacterPosition() = renderPos; } } @@ -1420,9 +1428,11 @@ void Application::update(float deltaTime) { glm::vec3 playerCanonical = core::coords::renderToCanonical(renderPos); constexpr float kM2BoardHorizDistSq = 12.0f * 12.0f; constexpr float kM2BoardVertDist = 15.0f; - constexpr float kTbLiftBoardHorizDistSq = 42.0f * 42.0f; + constexpr float kTbLiftBoardHorizDistSq = 95.0f * 95.0f; constexpr float kTbLiftBoardVertDist = 80.0f; + uint64_t bestGuid = 0; + float bestScore = 1e30f; for (auto& [guid, transport] : tm->getTransports()) { if (!transport.isM2) continue; const bool isThunderBluffLift = @@ -1437,9 +1447,18 @@ void Application::update(float deltaTime) { float horizDistSq = diff.x * diff.x + diff.y * diff.y; float vertDist = std::abs(diff.z); if (horizDistSq < maxHorizDistSq && vertDist < maxVertDist) { - gameHandler->setPlayerOnTransport(guid, playerCanonical - transport.position); - LOG_DEBUG("M2 transport boarding: guid=0x", std::hex, guid, std::dec); - break; + float score = horizDistSq + vertDist * vertDist; + if (score < bestScore) { + bestScore = score; + bestGuid = guid; + } + } + } + if (bestGuid != 0) { + auto* tr = tm->getTransport(bestGuid); + if (tr) { + gameHandler->setPlayerOnTransport(bestGuid, playerCanonical - tr->position); + LOG_DEBUG("M2 transport boarding: guid=0x", std::hex, bestGuid, std::dec); } } } @@ -1455,7 +1474,7 @@ void Application::update(float deltaTime) { const bool isThunderBluffLift = (tr->entry >= 20649u && tr->entry <= 20657u); constexpr float kM2DisembarkHorizDistSq = 15.0f * 15.0f; - constexpr float kTbLiftDisembarkHorizDistSq = 52.0f * 52.0f; + constexpr float kTbLiftDisembarkHorizDistSq = 120.0f * 120.0f; const float disembarkHorizDistSq = isThunderBluffLift ? kTbLiftDisembarkHorizDistSq : kM2DisembarkHorizDistSq; @@ -2902,6 +2921,12 @@ void Application::setupUICallbacks() { } transportManager->registerTransport(guid, wmoInstanceId, pathId, canonicalSpawnPos, entry); + // Keep type in sync with the spawned instance; needed for M2 lift boarding/motion. + if (!it->second.isWmo) { + if (auto* tr = transportManager->getTransport(guid)) { + tr->isM2 = true; + } + } } else { pendingTransportMoves_[guid] = PendingTransportMove{x, y, z, orientation}; LOG_DEBUG("Cannot auto-spawn transport 0x", std::hex, guid, std::dec, diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 0461ca59..16374768 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -309,6 +309,16 @@ bool isPlaceholderQuestTitle(const std::string& s) { return s.rfind("Quest #", 0) == 0; } +float mergeCooldownSeconds(float current, float incoming) { + constexpr float kEpsilon = 0.05f; + if (incoming <= 0.0f) return 0.0f; + if (current <= 0.0f) return incoming; + // Cooldowns should normally tick down. If a duplicate/late packet reports a + // larger value, keep the local remaining time to avoid visible timer resets. + if (incoming > current + kEpsilon) return current; + return incoming; +} + bool looksLikeQuestDescriptionText(const std::string& s) { int spaces = 0; int commas = 0; @@ -3208,7 +3218,14 @@ void GameHandler::handlePacket(network::Packet& packet) { uint32_t cdMs = packet.readUInt32(); float cdSec = cdMs / 1000.0f; if (cdSec > 0.0f) { - if (spellId != 0) spellCooldowns[spellId] = cdSec; + if (spellId != 0) { + auto it = spellCooldowns.find(spellId); + if (it == spellCooldowns.end()) { + spellCooldowns[spellId] = cdSec; + } else { + it->second = mergeCooldownSeconds(it->second, cdSec); + } + } // Resolve itemId from the GUID so item-type slots are also updated uint32_t itemId = 0; auto iit = onlineItems_.find(itemGuid); @@ -3217,8 +3234,14 @@ void GameHandler::handlePacket(network::Packet& packet) { bool match = (spellId != 0 && slot.type == ActionBarSlot::SPELL && slot.id == spellId) || (itemId != 0 && slot.type == ActionBarSlot::ITEM && slot.id == itemId); if (match) { - slot.cooldownTotal = cdSec; - slot.cooldownRemaining = cdSec; + float prevRemaining = slot.cooldownRemaining; + float merged = mergeCooldownSeconds(slot.cooldownRemaining, cdSec); + slot.cooldownRemaining = merged; + if (slot.cooldownTotal <= 0.0f || prevRemaining <= 0.0f) { + slot.cooldownTotal = cdSec; + } else { + slot.cooldownTotal = std::max(slot.cooldownTotal, merged); + } } } LOG_DEBUG("SMSG_ITEM_COOLDOWN: itemGuid=0x", std::hex, itemGuid, std::dec, @@ -9849,8 +9872,17 @@ void GameHandler::sendMovement(Opcode opcode) { sanitizeMovementForTaxi(); } - // Add transport data if player is on a transport - if (isOnTransport()) { + bool includeTransportInWire = isOnTransport(); + if (includeTransportInWire && transportManager_) { + if (auto* tr = transportManager_->getTransport(playerTransportGuid_); tr && tr->isM2) { + // Client-detected M2 elevators/trams are not always server-recognized transports. + // Sending ONTRANSPORT for these can trigger bad fall-state corrections server-side. + includeTransportInWire = false; + } + } + + // Add transport data if player is on a server-recognized transport + if (includeTransportInWire) { // Keep authoritative world position synchronized to parent transport transform // so heartbeats/corrections don't drag the passenger through geometry. if (transportManager_) { @@ -9892,7 +9924,7 @@ void GameHandler::sendMovement(Opcode opcode) { LOG_DEBUG("Sending movement packet: opcode=0x", std::hex, wireOpcode(opcode), std::dec, - (isOnTransport() ? " ONTRANSPORT" : "")); + (includeTransportInWire ? " ONTRANSPORT" : "")); // Convert canonical → server coordinates for the wire MovementInfo wireInfo = movementInfo; @@ -9905,7 +9937,7 @@ void GameHandler::sendMovement(Opcode opcode) { wireInfo.orientation = core::coords::canonicalToServerYaw(wireInfo.orientation); // Also convert transport local position to server coordinates if on transport - if (isOnTransport()) { + if (includeTransportInWire) { glm::vec3 serverTransportPos = core::coords::canonicalToServer( glm::vec3(wireInfo.transportX, wireInfo.transportY, wireInfo.transportZ)); wireInfo.transportX = serverTransportPos.x; @@ -16744,9 +16776,12 @@ void GameHandler::castSpell(uint32_t spellId, uint64_t targetGuid) { socket->send(packet); LOG_INFO("Casting spell: ", spellId, " on 0x", std::hex, target, std::dec); - // Optimistically start GCD immediately on cast — server will confirm or override - gcdTotal_ = 1.5f; - gcdStartedAt_ = std::chrono::steady_clock::now(); + // Optimistically start GCD immediately on cast, but do not restart it while + // already active (prevents timeout animation reset on repeated key presses). + if (!isGCDActive()) { + gcdTotal_ = 1.5f; + gcdStartedAt_ = std::chrono::steady_clock::now(); + } } void GameHandler::cancelCast() { @@ -17261,13 +17296,24 @@ void GameHandler::handleSpellCooldown(network::Packet& packet) { continue; } - spellCooldowns[spellId] = seconds; + auto it = spellCooldowns.find(spellId); + if (it == spellCooldowns.end()) { + spellCooldowns[spellId] = seconds; + } else { + it->second = mergeCooldownSeconds(it->second, seconds); + } for (auto& slot : actionBar) { bool match = (slot.type == ActionBarSlot::SPELL && slot.id == spellId) || (cdItemId != 0 && slot.type == ActionBarSlot::ITEM && slot.id == cdItemId); if (match) { - slot.cooldownTotal = seconds; - slot.cooldownRemaining = seconds; + float prevRemaining = slot.cooldownRemaining; + float merged = mergeCooldownSeconds(slot.cooldownRemaining, seconds); + slot.cooldownRemaining = merged; + if (slot.cooldownTotal <= 0.0f || prevRemaining <= 0.0f) { + slot.cooldownTotal = seconds; + } else { + slot.cooldownTotal = std::max(slot.cooldownTotal, merged); + } } } } diff --git a/src/game/transport_manager.cpp b/src/game/transport_manager.cpp index 955f8eaa..a9ff5cba 100644 --- a/src/game/transport_manager.cpp +++ b/src/game/transport_manager.cpp @@ -251,8 +251,10 @@ void TransportManager::updateTransportMovement(ActiveTransport& transport, float if (path.durationMs == 0) { // Just update transform (position already set) updateTransformMatrices(transport); - if (wmoRenderer_) { - wmoRenderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform); + if (transport.isM2) { + if (m2Renderer_) m2Renderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform); + } else { + if (wmoRenderer_) wmoRenderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform); } return; } @@ -287,8 +289,10 @@ void TransportManager::updateTransportMovement(ActiveTransport& transport, float } else { // Strict server-authoritative mode: do not guess movement between server snapshots. updateTransformMatrices(transport); - if (wmoRenderer_) { - wmoRenderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform); + if (transport.isM2) { + if (m2Renderer_) m2Renderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform); + } else { + if (wmoRenderer_) wmoRenderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform); } return; } @@ -777,8 +781,10 @@ void TransportManager::updateServerTransport(uint64_t guid, const glm::vec3& pos } updateTransformMatrices(*transport); - if (wmoRenderer_) { - wmoRenderer_->setInstanceTransform(transport->wmoInstanceId, transport->transform); + if (transport->isM2) { + if (m2Renderer_) m2Renderer_->setInstanceTransform(transport->wmoInstanceId, transport->transform); + } else { + if (wmoRenderer_) wmoRenderer_->setInstanceTransform(transport->wmoInstanceId, transport->transform); } return; }