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

@ -150,6 +150,7 @@ void TerrainManager::update(const Camera& camera, float deltaTime) {
}
// Always process ready tiles each frame (GPU uploads from background thread)
// Time budget prevents frame spikes from heavy tiles
processReadyTiles();
timeSinceLastUpdate += deltaTime;
@ -641,7 +642,8 @@ void TerrainManager::finalizeTile(const std::shared_ptr<PendingTile>& pending) {
m2Renderer->initialize(assetManager);
}
// Upload unique M2 models to GPU (stays in VRAM permanently until shutdown)
// Upload M2 models immediately (batching was causing hangs)
// The 5ms time budget in processReadyTiles() limits the spike
std::unordered_set<uint32_t> uploadedModelIds;
for (auto& m2Ready : pending->m2Models) {
if (m2Renderer->loadModel(m2Ready.model, m2Ready.modelId)) {
@ -649,7 +651,7 @@ void TerrainManager::finalizeTile(const std::shared_ptr<PendingTile>& pending) {
}
}
if (!uploadedModelIds.empty()) {
LOG_DEBUG(" Uploaded ", uploadedModelIds.size(), " unique M2 models to VRAM for tile [", x, ",", y, "]");
LOG_DEBUG(" Uploaded ", uploadedModelIds.size(), " M2 models for tile [", x, ",", y, "]");
}
// Create instances (deduplicate by uniqueId across tile boundaries)
@ -813,11 +815,13 @@ void TerrainManager::workerLoop() {
}
void TerrainManager::processReadyTiles() {
// Process up to 1 ready tile per frame to avoid main-thread stalls
// Process tiles with time budget to avoid frame spikes
// Budget: 5ms per frame (allows 3 tiles at ~1.5ms each or 1 heavy tile)
const float timeBudgetMs = 5.0f;
auto startTime = std::chrono::high_resolution_clock::now();
int processed = 0;
const int maxPerFrame = 1;
while (processed < maxPerFrame) {
while (true) {
std::shared_ptr<PendingTile> pending;
{
@ -831,16 +835,48 @@ void TerrainManager::processReadyTiles() {
if (pending) {
TileCoord coord = pending->coord;
auto tileStart = std::chrono::high_resolution_clock::now();
finalizeTile(pending);
auto tileEnd = std::chrono::high_resolution_clock::now();
float tileTimeMs = std::chrono::duration<float, std::milli>(tileEnd - tileStart).count();
{
std::lock_guard<std::mutex> lock(queueMutex);
pendingTiles.erase(coord);
}
processed++;
// Check if we've exceeded time budget
float elapsedMs = std::chrono::duration<float, std::milli>(tileEnd - startTime).count();
if (elapsedMs >= timeBudgetMs) {
if (processed > 1) {
LOG_DEBUG("Processed ", processed, " tiles in ", elapsedMs, "ms (budget: ", timeBudgetMs, "ms)");
}
break;
}
}
}
}
void TerrainManager::processM2UploadQueue() {
// Upload up to MAX_M2_UPLOADS_PER_FRAME models per frame
int uploaded = 0;
while (!m2UploadQueue_.empty() && uploaded < MAX_M2_UPLOADS_PER_FRAME) {
auto& upload = m2UploadQueue_.front();
if (m2Renderer) {
m2Renderer->loadModel(upload.model, upload.modelId);
}
m2UploadQueue_.pop();
uploaded++;
}
if (uploaded > 0) {
LOG_DEBUG("Uploaded ", uploaded, " M2 models (", m2UploadQueue_.size(), " remaining in queue)");
}
}
void TerrainManager::processAllReadyTiles() {
while (true) {
std::shared_ptr<PendingTile> pending;