fix(gameplay): keep timeout animation stable on repeated presses and harden M2 elevator sync

This commit is contained in:
Kelsi 2026-03-14 09:02:20 -07:00
parent f7a996ab26
commit 38210ec186
3 changed files with 111 additions and 34 deletions

View file

@ -504,16 +504,6 @@ void Application::run() {
LOG_INFO("Shadows: ", enabled ? "ON" : "OFF"); 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 // F8: Debug WMO floor at current position
else if (event.key.keysym.scancode == SDL_SCANCODE_F8 && event.key.repeat == 0) { else if (event.key.keysym.scancode == SDL_SCANCODE_F8 && event.key.repeat == 0) {
if (renderer && renderer->getWMORenderer()) { if (renderer && renderer->getWMORenderer()) {
@ -1376,6 +1366,24 @@ void Application::update(float deltaTime) {
} }
lastTransportCanonical = tr->position; lastTransportCanonical = tr->position;
lastTransportGuid = gameHandler->getPlayerTransportGuid(); 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); glm::vec3 playerCanonical = core::coords::renderToCanonical(renderPos);
constexpr float kM2BoardHorizDistSq = 12.0f * 12.0f; constexpr float kM2BoardHorizDistSq = 12.0f * 12.0f;
constexpr float kM2BoardVertDist = 15.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; constexpr float kTbLiftBoardVertDist = 80.0f;
uint64_t bestGuid = 0;
float bestScore = 1e30f;
for (auto& [guid, transport] : tm->getTransports()) { for (auto& [guid, transport] : tm->getTransports()) {
if (!transport.isM2) continue; if (!transport.isM2) continue;
const bool isThunderBluffLift = const bool isThunderBluffLift =
@ -1437,9 +1447,18 @@ void Application::update(float deltaTime) {
float horizDistSq = diff.x * diff.x + diff.y * diff.y; float horizDistSq = diff.x * diff.x + diff.y * diff.y;
float vertDist = std::abs(diff.z); float vertDist = std::abs(diff.z);
if (horizDistSq < maxHorizDistSq && vertDist < maxVertDist) { if (horizDistSq < maxHorizDistSq && vertDist < maxVertDist) {
gameHandler->setPlayerOnTransport(guid, playerCanonical - transport.position); float score = horizDistSq + vertDist * vertDist;
LOG_DEBUG("M2 transport boarding: guid=0x", std::hex, guid, std::dec); if (score < bestScore) {
break; 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 = const bool isThunderBluffLift =
(tr->entry >= 20649u && tr->entry <= 20657u); (tr->entry >= 20649u && tr->entry <= 20657u);
constexpr float kM2DisembarkHorizDistSq = 15.0f * 15.0f; 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 const float disembarkHorizDistSq = isThunderBluffLift
? kTbLiftDisembarkHorizDistSq ? kTbLiftDisembarkHorizDistSq
: kM2DisembarkHorizDistSq; : kM2DisembarkHorizDistSq;
@ -2902,6 +2921,12 @@ void Application::setupUICallbacks() {
} }
transportManager->registerTransport(guid, wmoInstanceId, pathId, canonicalSpawnPos, entry); 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 { } else {
pendingTransportMoves_[guid] = PendingTransportMove{x, y, z, orientation}; pendingTransportMoves_[guid] = PendingTransportMove{x, y, z, orientation};
LOG_DEBUG("Cannot auto-spawn transport 0x", std::hex, guid, std::dec, LOG_DEBUG("Cannot auto-spawn transport 0x", std::hex, guid, std::dec,

View file

@ -309,6 +309,16 @@ bool isPlaceholderQuestTitle(const std::string& s) {
return s.rfind("Quest #", 0) == 0; 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) { bool looksLikeQuestDescriptionText(const std::string& s) {
int spaces = 0; int spaces = 0;
int commas = 0; int commas = 0;
@ -3208,7 +3218,14 @@ void GameHandler::handlePacket(network::Packet& packet) {
uint32_t cdMs = packet.readUInt32(); uint32_t cdMs = packet.readUInt32();
float cdSec = cdMs / 1000.0f; float cdSec = cdMs / 1000.0f;
if (cdSec > 0.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 // Resolve itemId from the GUID so item-type slots are also updated
uint32_t itemId = 0; uint32_t itemId = 0;
auto iit = onlineItems_.find(itemGuid); 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) bool match = (spellId != 0 && slot.type == ActionBarSlot::SPELL && slot.id == spellId)
|| (itemId != 0 && slot.type == ActionBarSlot::ITEM && slot.id == itemId); || (itemId != 0 && slot.type == ActionBarSlot::ITEM && slot.id == itemId);
if (match) { if (match) {
slot.cooldownTotal = cdSec; float prevRemaining = slot.cooldownRemaining;
slot.cooldownRemaining = cdSec; 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, LOG_DEBUG("SMSG_ITEM_COOLDOWN: itemGuid=0x", std::hex, itemGuid, std::dec,
@ -9849,8 +9872,17 @@ void GameHandler::sendMovement(Opcode opcode) {
sanitizeMovementForTaxi(); sanitizeMovementForTaxi();
} }
// Add transport data if player is on a transport bool includeTransportInWire = isOnTransport();
if (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 // Keep authoritative world position synchronized to parent transport transform
// so heartbeats/corrections don't drag the passenger through geometry. // so heartbeats/corrections don't drag the passenger through geometry.
if (transportManager_) { if (transportManager_) {
@ -9892,7 +9924,7 @@ void GameHandler::sendMovement(Opcode opcode) {
LOG_DEBUG("Sending movement packet: opcode=0x", std::hex, LOG_DEBUG("Sending movement packet: opcode=0x", std::hex,
wireOpcode(opcode), std::dec, wireOpcode(opcode), std::dec,
(isOnTransport() ? " ONTRANSPORT" : "")); (includeTransportInWire ? " ONTRANSPORT" : ""));
// Convert canonical → server coordinates for the wire // Convert canonical → server coordinates for the wire
MovementInfo wireInfo = movementInfo; MovementInfo wireInfo = movementInfo;
@ -9905,7 +9937,7 @@ void GameHandler::sendMovement(Opcode opcode) {
wireInfo.orientation = core::coords::canonicalToServerYaw(wireInfo.orientation); wireInfo.orientation = core::coords::canonicalToServerYaw(wireInfo.orientation);
// Also convert transport local position to server coordinates if on transport // Also convert transport local position to server coordinates if on transport
if (isOnTransport()) { if (includeTransportInWire) {
glm::vec3 serverTransportPos = core::coords::canonicalToServer( glm::vec3 serverTransportPos = core::coords::canonicalToServer(
glm::vec3(wireInfo.transportX, wireInfo.transportY, wireInfo.transportZ)); glm::vec3(wireInfo.transportX, wireInfo.transportY, wireInfo.transportZ));
wireInfo.transportX = serverTransportPos.x; wireInfo.transportX = serverTransportPos.x;
@ -16744,9 +16776,12 @@ void GameHandler::castSpell(uint32_t spellId, uint64_t targetGuid) {
socket->send(packet); socket->send(packet);
LOG_INFO("Casting spell: ", spellId, " on 0x", std::hex, target, std::dec); LOG_INFO("Casting spell: ", spellId, " on 0x", std::hex, target, std::dec);
// Optimistically start GCD immediately on cast — server will confirm or override // Optimistically start GCD immediately on cast, but do not restart it while
gcdTotal_ = 1.5f; // already active (prevents timeout animation reset on repeated key presses).
gcdStartedAt_ = std::chrono::steady_clock::now(); if (!isGCDActive()) {
gcdTotal_ = 1.5f;
gcdStartedAt_ = std::chrono::steady_clock::now();
}
} }
void GameHandler::cancelCast() { void GameHandler::cancelCast() {
@ -17261,13 +17296,24 @@ void GameHandler::handleSpellCooldown(network::Packet& packet) {
continue; 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) { for (auto& slot : actionBar) {
bool match = (slot.type == ActionBarSlot::SPELL && slot.id == spellId) bool match = (slot.type == ActionBarSlot::SPELL && slot.id == spellId)
|| (cdItemId != 0 && slot.type == ActionBarSlot::ITEM && slot.id == cdItemId); || (cdItemId != 0 && slot.type == ActionBarSlot::ITEM && slot.id == cdItemId);
if (match) { if (match) {
slot.cooldownTotal = seconds; float prevRemaining = slot.cooldownRemaining;
slot.cooldownRemaining = seconds; 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);
}
} }
} }
} }

View file

@ -251,8 +251,10 @@ void TransportManager::updateTransportMovement(ActiveTransport& transport, float
if (path.durationMs == 0) { if (path.durationMs == 0) {
// Just update transform (position already set) // Just update transform (position already set)
updateTransformMatrices(transport); updateTransformMatrices(transport);
if (wmoRenderer_) { if (transport.isM2) {
wmoRenderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform); if (m2Renderer_) m2Renderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform);
} else {
if (wmoRenderer_) wmoRenderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform);
} }
return; return;
} }
@ -287,8 +289,10 @@ void TransportManager::updateTransportMovement(ActiveTransport& transport, float
} else { } else {
// Strict server-authoritative mode: do not guess movement between server snapshots. // Strict server-authoritative mode: do not guess movement between server snapshots.
updateTransformMatrices(transport); updateTransformMatrices(transport);
if (wmoRenderer_) { if (transport.isM2) {
wmoRenderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform); if (m2Renderer_) m2Renderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform);
} else {
if (wmoRenderer_) wmoRenderer_->setInstanceTransform(transport.wmoInstanceId, transport.transform);
} }
return; return;
} }
@ -777,8 +781,10 @@ void TransportManager::updateServerTransport(uint64_t guid, const glm::vec3& pos
} }
updateTransformMatrices(*transport); updateTransformMatrices(*transport);
if (wmoRenderer_) { if (transport->isM2) {
wmoRenderer_->setInstanceTransform(transport->wmoInstanceId, transport->transform); if (m2Renderer_) m2Renderer_->setInstanceTransform(transport->wmoInstanceId, transport->transform);
} else {
if (wmoRenderer_) wmoRenderer_->setInstanceTransform(transport->wmoInstanceId, transport->transform);
} }
return; return;
} }