Add property-based mount animation discovery and procedural lean

Mount Animation System:
- Property-based jump animation discovery using sequence metadata
- Chain linkage scoring (nextAnimation/aliasNext) for accurate detection
- Correct loop detection: flags & 0x01 == 0 means looping
- Avoids brake/stop animations via blendTime penalties
- Works on any mount model without hardcoded animation IDs

Mount Physics:
- Physics-based jump height: vz = sqrt(2 * g * h)
- Configurable MOUNT_JUMP_HEIGHT constant (1.0m default)
- Procedural lean into turns for ground mounts
- Smooth roll based on turn rate (±14° max, 6x/sec blend)

Audio Improvements:
- State-machine driven mount sounds (jump, land, rear-up)
- Semantic sound methods (no animation ID dependencies)
- Debug logging for missing sound files

Bug Fixes:
- Fixed mount animation sequencing (JumpStart → JumpLoop → JumpEnd)
- Fixed animation loop flag interpretation (0x20 vs 0x21)
- Rider bone attachment working correctly during all mount actions
This commit is contained in:
Kelsi 2026-02-10 19:30:45 -08:00
parent 3c783d1845
commit c623fcef51
16 changed files with 1083 additions and 145 deletions

View file

@ -118,6 +118,16 @@ bool GameHandler::isConnected() const {
}
void GameHandler::update(float deltaTime) {
// Timing profiling (log every 60 frames to reduce spam)
static int profileCounter = 0;
static float socketTime = 0.0f;
static float taxiTime = 0.0f;
static float distanceCheckTime = 0.0f;
static float entityUpdateTime = 0.0f;
static float totalTime = 0.0f;
auto updateStart = std::chrono::high_resolution_clock::now();
// Fire deferred char-create callback (outside ImGui render)
if (pendingCharCreateResult_) {
pendingCharCreateResult_ = false;
@ -131,9 +141,12 @@ void GameHandler::update(float deltaTime) {
}
// Update socket (processes incoming data and triggers callbacks)
auto socketStart = std::chrono::high_resolution_clock::now();
if (socket) {
socket->update();
}
auto socketEnd = std::chrono::high_resolution_clock::now();
socketTime += std::chrono::duration<float, std::milli>(socketEnd - socketStart).count();
// Validate target still exists
if (targetGuid != 0 && !entityManager.hasEntity(targetGuid)) {
@ -187,6 +200,9 @@ void GameHandler::update(float deltaTime) {
taxiLandingCooldown_ -= deltaTime;
}
// Taxi logic timing
auto taxiStart = std::chrono::high_resolution_clock::now();
// Detect taxi flight landing: UNIT_FLAG_TAXI_FLIGHT (0x00000100) cleared
if (onTaxiFlight_) {
updateClientTaxi(deltaTime);
@ -286,6 +302,12 @@ void GameHandler::update(float deltaTime) {
}
}
auto taxiEnd = std::chrono::high_resolution_clock::now();
taxiTime += std::chrono::duration<float, std::milli>(taxiEnd - taxiStart).count();
// Distance check timing
auto distanceStart = std::chrono::high_resolution_clock::now();
// Leave combat if auto-attack target is too far away (leash range)
if (autoAttacking && autoAttackTarget != 0) {
auto targetEntity = entityManager.getEntity(autoAttackTarget);
@ -350,10 +372,51 @@ void GameHandler::update(float deltaTime) {
}
}
auto distanceEnd = std::chrono::high_resolution_clock::now();
distanceCheckTime += std::chrono::duration<float, std::milli>(distanceEnd - distanceStart).count();
// Entity update timing
auto entityStart = std::chrono::high_resolution_clock::now();
// Update entity movement interpolation (keeps targeting in sync with visuals)
// Only update entities within reasonable distance for performance
const float updateRadiusSq = 150.0f * 150.0f; // 150 unit radius
auto playerEntity = entityManager.getEntity(playerGuid);
glm::vec3 playerPos = playerEntity ? glm::vec3(playerEntity->getX(), playerEntity->getY(), playerEntity->getZ()) : glm::vec3(0.0f);
for (auto& [guid, entity] : entityManager.getEntities()) {
entity->updateMovement(deltaTime);
// Always update player
if (guid == playerGuid) {
entity->updateMovement(deltaTime);
continue;
}
// Distance cull other entities
glm::vec3 entityPos(entity->getX(), entity->getY(), entity->getZ());
float distSq = glm::dot(entityPos - playerPos, entityPos - playerPos);
if (distSq < updateRadiusSq) {
entity->updateMovement(deltaTime);
}
}
auto entityEnd = std::chrono::high_resolution_clock::now();
entityUpdateTime += std::chrono::duration<float, std::milli>(entityEnd - entityStart).count();
}
auto updateEnd = std::chrono::high_resolution_clock::now();
totalTime += std::chrono::duration<float, std::milli>(updateEnd - updateStart).count();
// Log profiling every 60 frames
if (++profileCounter >= 60) {
LOG_INFO("UPDATE PROFILE (60 frames): socket=", socketTime / 60.0f, "ms taxi=", taxiTime / 60.0f,
"ms distance=", distanceCheckTime / 60.0f, "ms entity=", entityUpdateTime / 60.0f,
"ms TOTAL=", totalTime / 60.0f, "ms");
profileCounter = 0;
socketTime = 0.0f;
taxiTime = 0.0f;
distanceCheckTime = 0.0f;
entityUpdateTime = 0.0f;
totalTime = 0.0f;
}
}
@ -418,6 +481,11 @@ void GameHandler::handlePacket(network::Packet& packet) {
}
break;
case Opcode::SMSG_LOGIN_SETTIMESPEED:
// Can be received during login or at any time after
handleLoginSetTimeSpeed(packet);
break;
case Opcode::SMSG_ACCOUNT_DATA_TIMES:
// Can be received at any time after authentication
handleAccountDataTimes(packet);
@ -1472,6 +1540,28 @@ void GameHandler::selectCharacter(uint64_t characterGuid) {
LOG_INFO("CMSG_PLAYER_LOGIN sent, entering world...");
}
void GameHandler::handleLoginSetTimeSpeed(network::Packet& packet) {
// SMSG_LOGIN_SETTIMESPEED (0x042)
// Structure: uint32 gameTime, float timeScale
// gameTime: Game time in seconds since epoch
// timeScale: Time speed multiplier (typically 0.0166 for 1 day = 1 hour)
if (packet.getSize() < 8) {
LOG_WARNING("SMSG_LOGIN_SETTIMESPEED: packet too small (", packet.getSize(), " bytes)");
return;
}
uint32_t gameTimePacked = packet.readUInt32();
float timeScale = packet.readFloat();
// Store for celestial/sky system use
gameTime_ = static_cast<float>(gameTimePacked);
timeSpeed_ = timeScale;
LOG_INFO("Server time: gameTime=", gameTime_, "s, timeSpeed=", timeSpeed_);
LOG_INFO(" (1 game day = ", (1.0f / timeSpeed_) / 60.0f, " real minutes)");
}
void GameHandler::handleLoginVerifyWorld(network::Packet& packet) {
LOG_INFO("Handling SMSG_LOGIN_VERIFY_WORLD");