From 35fff9307da223c1d9911c9d98b7e304d90c3e3c Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 7 Feb 2026 18:38:36 -0800 Subject: [PATCH] Add mount rider bob and hoofbeat sounds, improve world map - Rider character bobs with mount's run animation (sinusoidal, 0.12u amplitude) - Mount hoofbeat footstep sounds triggered at 4 points per animation cycle - M key opens map directly to player's current zone instead of continent - Mouse wheel scroll zooms map in/out between world, continent, and zone views - Fog of war darkens unexplored zones on continent view, clears on visit --- include/rendering/renderer.hpp | 5 ++ include/rendering/world_map.hpp | 8 +++ src/rendering/renderer.cpp | 54 +++++++++++++-- src/rendering/world_map.cpp | 118 ++++++++++++++++++++++++++++++-- 4 files changed, 174 insertions(+), 11 deletions(-) diff --git a/include/rendering/renderer.hpp b/include/rendering/renderer.hpp index c2bb0265..bf003bad 100644 --- a/include/rendering/renderer.hpp +++ b/include/rendering/renderer.hpp @@ -252,6 +252,11 @@ private: uint32_t footstepLastAnimationId = 0; float footstepLastNormTime = 0.0f; bool footstepNormInitialized = false; + + // Mount footstep tracking (separate from player's) + uint32_t mountFootstepLastAnimId = 0; + float mountFootstepLastNormTime = 0.0f; + bool mountFootstepNormInitialized = false; bool sfxStateInitialized = false; bool sfxPrevGrounded = true; bool sfxPrevJumping = false; diff --git a/include/rendering/world_map.hpp b/include/rendering/world_map.hpp index e501495c..b2b7c420 100644 --- a/include/rendering/world_map.hpp +++ b/include/rendering/world_map.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include namespace wowee { @@ -45,12 +46,16 @@ private: void enterWorldView(); void loadZonesFromDBC(); int findBestContinentForPlayer(const glm::vec3& playerRenderPos) const; + int findZoneForPlayer(const glm::vec3& playerRenderPos) const; bool zoneBelongsToContinent(int zoneIdx, int contIdx) const; bool getContinentProjectionBounds(int contIdx, float& left, float& right, float& top, float& bottom) const; void loadZoneTextures(int zoneIdx); void compositeZone(int zoneIdx); void renderImGuiOverlay(const glm::vec3& playerRenderPos, int screenWidth, int screenHeight); + void updateExploration(const glm::vec3& playerRenderPos); + void zoomIn(const glm::vec3& playerRenderPos); + void zoomOut(); // World pos → map UV using a specific zone's bounds glm::vec2 renderPosToMapUV(const glm::vec3& renderPos, int zoneIdx) const; @@ -80,6 +85,9 @@ private: std::unique_ptr tileShader; GLuint tileQuadVAO = 0; GLuint tileQuadVBO = 0; + + // Exploration / fog of war + std::unordered_set exploredZones; // zone indices the player has visited }; } // namespace rendering diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index 1654f8a0..0b5a118f 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -518,6 +518,7 @@ void Renderer::updateCharacterAnimation() { } // Sync mount instance position and rotation + float mountBob = 0.0f; if (mountInstanceId_ > 0) { characterRenderer->setInstancePosition(mountInstanceId_, characterPosition); float yawRad = glm::radians(characterYaw); @@ -531,11 +532,18 @@ void Renderer::updateCharacterAnimation() { if (!haveMountState || curMountAnim != mountAnimId) { characterRenderer->playAnimation(mountInstanceId_, mountAnimId, true); } + + // Rider bob: sinusoidal motion synced to mount's run animation + if (moving && haveMountState && curMountDur > 1.0f) { + float norm = std::fmod(curMountTime, curMountDur) / curMountDur; + // Two bounces per stride cycle (horse gait), lowest at footfalls (0.22, 0.72) + mountBob = std::sin(norm * 4.0f * 3.14159f) * 0.12f; + } } - // Offset player Z above mount + // Offset player Z above mount + bob glm::vec3 playerPos = characterPosition; - playerPos.z += mountHeightOffset_; + playerPos.z += mountHeightOffset_ + mountBob; characterRenderer->setInstancePosition(characterInstanceId, playerPos); return; } @@ -993,10 +1001,44 @@ void Renderer::update(float deltaTime) { // Footsteps: animation-event driven + surface query at event time. if (footstepManager) { footstepManager->update(deltaTime); - if (characterRenderer && characterInstanceId > 0 && + bool canPlayFootsteps = characterRenderer && characterInstanceId > 0 && cameraController && cameraController->isThirdPerson() && - isFootstepAnimationState() && cameraController->isGrounded() && - !cameraController->isSwimming()) { + cameraController->isGrounded() && !cameraController->isSwimming(); + + if (canPlayFootsteps && isMounted() && mountInstanceId_ > 0) { + // Mount footsteps: use mount's animation for timing + uint32_t animId = 0; + float animTimeMs = 0.0f, animDurationMs = 0.0f; + if (characterRenderer->getAnimationState(mountInstanceId_, animId, animTimeMs, animDurationMs) && + animDurationMs > 1.0f && cameraController->isMoving()) { + float norm = std::fmod(animTimeMs, animDurationMs) / animDurationMs; + if (norm < 0.0f) norm += 1.0f; + + if (animId != mountFootstepLastAnimId) { + mountFootstepLastAnimId = animId; + mountFootstepLastNormTime = norm; + mountFootstepNormInitialized = true; + } else if (!mountFootstepNormInitialized) { + mountFootstepNormInitialized = true; + mountFootstepLastNormTime = norm; + } else { + // Horse gait: 4 hoofbeats per cycle + auto crossed = [&](float eventNorm) { + if (mountFootstepLastNormTime <= norm) { + return mountFootstepLastNormTime < eventNorm && eventNorm <= norm; + } + return mountFootstepLastNormTime < eventNorm || eventNorm <= norm; + }; + if (crossed(0.1f) || crossed(0.35f) || crossed(0.6f) || crossed(0.85f)) { + footstepManager->playFootstep(resolveFootstepSurface(), true); + } + mountFootstepLastNormTime = norm; + } + } else { + mountFootstepNormInitialized = false; + } + footstepNormInitialized = false; // Reset player footstep tracking + } else if (canPlayFootsteps && isFootstepAnimationState()) { uint32_t animId = 0; float animTimeMs = 0.0f; float animDurationMs = 0.0f; @@ -1004,8 +1046,10 @@ void Renderer::update(float deltaTime) { shouldTriggerFootstepEvent(animId, animTimeMs, animDurationMs)) { footstepManager->playFootstep(resolveFootstepSurface(), cameraController->isSprinting()); } + mountFootstepNormInitialized = false; } else { footstepNormInitialized = false; + mountFootstepNormInitialized = false; } } diff --git a/src/rendering/world_map.cpp b/src/rendering/world_map.cpp index 0d1cfeff..156f8dae 100644 --- a/src/rendering/world_map.cpp +++ b/src/rendering/world_map.cpp @@ -347,6 +347,38 @@ int WorldMap::findBestContinentForPlayer(const glm::vec3& playerRenderPos) const return bestIdx; } +int WorldMap::findZoneForPlayer(const glm::vec3& playerRenderPos) const { + float wowX = playerRenderPos.y; // north/south + float wowY = playerRenderPos.x; // west/east + + int bestIdx = -1; + float bestArea = std::numeric_limits::max(); + + for (int i = 0; i < static_cast(zones.size()); i++) { + const auto& z = zones[i]; + if (z.areaID == 0) continue; // skip continent-level entries + + float minX = std::min(z.locLeft, z.locRight); + float maxX = std::max(z.locLeft, z.locRight); + float minY = std::min(z.locTop, z.locBottom); + float maxY = std::max(z.locTop, z.locBottom); + float spanX = maxX - minX; + float spanY = maxY - minY; + if (spanX < 0.001f || spanY < 0.001f) continue; + + bool contains = (wowX >= minX && wowX <= maxX && wowY >= minY && wowY <= maxY); + if (contains) { + float area = spanX * spanY; + if (area < bestArea) { + bestArea = area; + bestIdx = i; + } + } + } + + return bestIdx; +} + bool WorldMap::zoneBelongsToContinent(int zoneIdx, int contIdx) const { if (zoneIdx < 0 || zoneIdx >= static_cast(zones.size())) return false; if (contIdx < 0 || contIdx >= static_cast(zones.size())) return false; @@ -691,6 +723,52 @@ glm::vec2 WorldMap::renderPosToMapUV(const glm::vec3& renderPos, int zoneIdx) co return glm::vec2(u, v); } +// -------------------------------------------------------- +// Exploration tracking +// -------------------------------------------------------- + +void WorldMap::updateExploration(const glm::vec3& playerRenderPos) { + int zoneIdx = findZoneForPlayer(playerRenderPos); + if (zoneIdx >= 0) { + exploredZones.insert(zoneIdx); + } +} + +void WorldMap::zoomIn(const glm::vec3& playerRenderPos) { + if (viewLevel == ViewLevel::WORLD) { + // World → Continent + if (continentIdx >= 0) { + loadZoneTextures(continentIdx); + compositeZone(continentIdx); + currentIdx = continentIdx; + viewLevel = ViewLevel::CONTINENT; + } + } else if (viewLevel == ViewLevel::CONTINENT) { + // Continent → Zone (use player's current zone) + int zoneIdx = findZoneForPlayer(playerRenderPos); + if (zoneIdx >= 0 && zoneBelongsToContinent(zoneIdx, continentIdx)) { + loadZoneTextures(zoneIdx); + compositeZone(zoneIdx); + currentIdx = zoneIdx; + viewLevel = ViewLevel::ZONE; + } + } +} + +void WorldMap::zoomOut() { + if (viewLevel == ViewLevel::ZONE) { + // Zone → Continent + if (continentIdx >= 0) { + compositeZone(continentIdx); + currentIdx = continentIdx; + viewLevel = ViewLevel::CONTINENT; + } + } else if (viewLevel == ViewLevel::CONTINENT) { + // Continent → World + enterWorldView(); + } +} + // -------------------------------------------------------- // Main render // -------------------------------------------------------- @@ -700,6 +778,11 @@ void WorldMap::render(const glm::vec3& playerRenderPos, int screenWidth, int scr auto& input = core::Input::getInstance(); + // Track exploration even when map is closed + if (!zones.empty()) { + updateExploration(playerRenderPos); + } + // When map is open, always allow M/Escape to close (bypass ImGui keyboard capture) if (open) { if (input.isKeyJustPressed(SDL_SCANCODE_M) || @@ -707,6 +790,14 @@ void WorldMap::render(const glm::vec3& playerRenderPos, int screenWidth, int scr open = false; return; } + + // Mouse wheel: scroll up = zoom in, scroll down = zoom out + auto& io = ImGui::GetIO(); + if (io.MouseWheel > 0.0f) { + zoomIn(playerRenderPos); + } else if (io.MouseWheel < 0.0f) { + zoomOut(); + } } else { auto& io = ImGui::GetIO(); if (!io.WantCaptureKeyboard && input.isKeyJustPressed(SDL_SCANCODE_M)) { @@ -721,8 +812,15 @@ void WorldMap::render(const glm::vec3& playerRenderPos, int screenWidth, int scr compositedIdx = -1; } - // Ensure continent textures are loaded and composited - if (continentIdx >= 0) { + // Open directly to the player's current zone + int playerZone = findZoneForPlayer(playerRenderPos); + if (playerZone >= 0 && continentIdx >= 0 && + zoneBelongsToContinent(playerZone, continentIdx)) { + loadZoneTextures(playerZone); + compositeZone(playerZone); + currentIdx = playerZone; + viewLevel = ViewLevel::ZONE; + } else if (continentIdx >= 0) { loadZoneTextures(continentIdx); compositeZone(continentIdx); currentIdx = continentIdx; @@ -942,17 +1040,25 @@ void WorldMap::renderImGuiOverlay(const glm::vec3& playerRenderPos, int screenWi float sx1 = imgMin.x + zuMax * displayW; float sy1 = imgMin.y + zvMax * displayH; + bool explored = exploredZones.count(zi) > 0; + // Check hover bool hovered = (mousePos.x >= sx0 && mousePos.x <= sx1 && mousePos.y >= sy0 && mousePos.y <= sy1); + // Fog of war: darken unexplored zones + if (!explored) { + drawList->AddRectFilled(ImVec2(sx0, sy0), ImVec2(sx1, sy1), + IM_COL32(0, 0, 0, 160)); + } + if (hovered) { hoveredZone = zi; drawList->AddRectFilled(ImVec2(sx0, sy0), ImVec2(sx1, sy1), IM_COL32(255, 255, 200, 40)); drawList->AddRect(ImVec2(sx0, sy0), ImVec2(sx1, sy1), IM_COL32(255, 215, 0, 180), 0.0f, 0, 2.0f); - } else { + } else if (explored) { drawList->AddRect(ImVec2(sx0, sy0), ImVec2(sx1, sy1), IM_COL32(255, 255, 255, 30), 0.0f, 0, 1.0f); } @@ -1022,11 +1128,11 @@ void WorldMap::renderImGuiOverlay(const glm::vec3& playerRenderPos, int screenWi // Help text const char* helpText; if (viewLevel == ViewLevel::ZONE) { - helpText = "Right-click to zoom out | M or Escape to close"; + helpText = "Scroll out or right-click to zoom out | M or Escape to close"; } else if (viewLevel == ViewLevel::WORLD) { - helpText = "Select a continent | M or Escape to close"; + helpText = "Select a continent | Scroll in to zoom | M or Escape to close"; } else { - helpText = "Click a zone to zoom in | Right-click for World | M or Escape to close"; + helpText = "Click zone or scroll in to zoom | Scroll out / right-click for World | M or Escape to close"; } ImVec2 textSize = ImGui::CalcTextSize(helpText); float textY = mapY + displayH + 8.0f;