mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-03 00:03:50 +00:00
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
This commit is contained in:
parent
a123d2a49a
commit
549b4f63bf
4 changed files with 174 additions and 11 deletions
|
|
@ -252,6 +252,11 @@ private:
|
||||||
uint32_t footstepLastAnimationId = 0;
|
uint32_t footstepLastAnimationId = 0;
|
||||||
float footstepLastNormTime = 0.0f;
|
float footstepLastNormTime = 0.0f;
|
||||||
bool footstepNormInitialized = false;
|
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 sfxStateInitialized = false;
|
||||||
bool sfxPrevGrounded = true;
|
bool sfxPrevGrounded = true;
|
||||||
bool sfxPrevJumping = false;
|
bool sfxPrevJumping = false;
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
#include <glm/glm.hpp>
|
#include <glm/glm.hpp>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <unordered_set>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace wowee {
|
namespace wowee {
|
||||||
|
|
@ -45,12 +46,16 @@ private:
|
||||||
void enterWorldView();
|
void enterWorldView();
|
||||||
void loadZonesFromDBC();
|
void loadZonesFromDBC();
|
||||||
int findBestContinentForPlayer(const glm::vec3& playerRenderPos) const;
|
int findBestContinentForPlayer(const glm::vec3& playerRenderPos) const;
|
||||||
|
int findZoneForPlayer(const glm::vec3& playerRenderPos) const;
|
||||||
bool zoneBelongsToContinent(int zoneIdx, int contIdx) const;
|
bool zoneBelongsToContinent(int zoneIdx, int contIdx) const;
|
||||||
bool getContinentProjectionBounds(int contIdx, float& left, float& right,
|
bool getContinentProjectionBounds(int contIdx, float& left, float& right,
|
||||||
float& top, float& bottom) const;
|
float& top, float& bottom) const;
|
||||||
void loadZoneTextures(int zoneIdx);
|
void loadZoneTextures(int zoneIdx);
|
||||||
void compositeZone(int zoneIdx);
|
void compositeZone(int zoneIdx);
|
||||||
void renderImGuiOverlay(const glm::vec3& playerRenderPos, int screenWidth, int screenHeight);
|
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
|
// World pos → map UV using a specific zone's bounds
|
||||||
glm::vec2 renderPosToMapUV(const glm::vec3& renderPos, int zoneIdx) const;
|
glm::vec2 renderPosToMapUV(const glm::vec3& renderPos, int zoneIdx) const;
|
||||||
|
|
@ -80,6 +85,9 @@ private:
|
||||||
std::unique_ptr<Shader> tileShader;
|
std::unique_ptr<Shader> tileShader;
|
||||||
GLuint tileQuadVAO = 0;
|
GLuint tileQuadVAO = 0;
|
||||||
GLuint tileQuadVBO = 0;
|
GLuint tileQuadVBO = 0;
|
||||||
|
|
||||||
|
// Exploration / fog of war
|
||||||
|
std::unordered_set<int> exploredZones; // zone indices the player has visited
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace rendering
|
} // namespace rendering
|
||||||
|
|
|
||||||
|
|
@ -518,6 +518,7 @@ void Renderer::updateCharacterAnimation() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync mount instance position and rotation
|
// Sync mount instance position and rotation
|
||||||
|
float mountBob = 0.0f;
|
||||||
if (mountInstanceId_ > 0) {
|
if (mountInstanceId_ > 0) {
|
||||||
characterRenderer->setInstancePosition(mountInstanceId_, characterPosition);
|
characterRenderer->setInstancePosition(mountInstanceId_, characterPosition);
|
||||||
float yawRad = glm::radians(characterYaw);
|
float yawRad = glm::radians(characterYaw);
|
||||||
|
|
@ -531,11 +532,18 @@ void Renderer::updateCharacterAnimation() {
|
||||||
if (!haveMountState || curMountAnim != mountAnimId) {
|
if (!haveMountState || curMountAnim != mountAnimId) {
|
||||||
characterRenderer->playAnimation(mountInstanceId_, mountAnimId, true);
|
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;
|
glm::vec3 playerPos = characterPosition;
|
||||||
playerPos.z += mountHeightOffset_;
|
playerPos.z += mountHeightOffset_ + mountBob;
|
||||||
characterRenderer->setInstancePosition(characterInstanceId, playerPos);
|
characterRenderer->setInstancePosition(characterInstanceId, playerPos);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -993,10 +1001,44 @@ void Renderer::update(float deltaTime) {
|
||||||
// Footsteps: animation-event driven + surface query at event time.
|
// Footsteps: animation-event driven + surface query at event time.
|
||||||
if (footstepManager) {
|
if (footstepManager) {
|
||||||
footstepManager->update(deltaTime);
|
footstepManager->update(deltaTime);
|
||||||
if (characterRenderer && characterInstanceId > 0 &&
|
bool canPlayFootsteps = characterRenderer && characterInstanceId > 0 &&
|
||||||
cameraController && cameraController->isThirdPerson() &&
|
cameraController && cameraController->isThirdPerson() &&
|
||||||
isFootstepAnimationState() && cameraController->isGrounded() &&
|
cameraController->isGrounded() && !cameraController->isSwimming();
|
||||||
!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;
|
uint32_t animId = 0;
|
||||||
float animTimeMs = 0.0f;
|
float animTimeMs = 0.0f;
|
||||||
float animDurationMs = 0.0f;
|
float animDurationMs = 0.0f;
|
||||||
|
|
@ -1004,8 +1046,10 @@ void Renderer::update(float deltaTime) {
|
||||||
shouldTriggerFootstepEvent(animId, animTimeMs, animDurationMs)) {
|
shouldTriggerFootstepEvent(animId, animTimeMs, animDurationMs)) {
|
||||||
footstepManager->playFootstep(resolveFootstepSurface(), cameraController->isSprinting());
|
footstepManager->playFootstep(resolveFootstepSurface(), cameraController->isSprinting());
|
||||||
}
|
}
|
||||||
|
mountFootstepNormInitialized = false;
|
||||||
} else {
|
} else {
|
||||||
footstepNormInitialized = false;
|
footstepNormInitialized = false;
|
||||||
|
mountFootstepNormInitialized = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -347,6 +347,38 @@ int WorldMap::findBestContinentForPlayer(const glm::vec3& playerRenderPos) const
|
||||||
return bestIdx;
|
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<float>::max();
|
||||||
|
|
||||||
|
for (int i = 0; i < static_cast<int>(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 {
|
bool WorldMap::zoneBelongsToContinent(int zoneIdx, int contIdx) const {
|
||||||
if (zoneIdx < 0 || zoneIdx >= static_cast<int>(zones.size())) return false;
|
if (zoneIdx < 0 || zoneIdx >= static_cast<int>(zones.size())) return false;
|
||||||
if (contIdx < 0 || contIdx >= static_cast<int>(zones.size())) return false;
|
if (contIdx < 0 || contIdx >= static_cast<int>(zones.size())) return false;
|
||||||
|
|
@ -691,6 +723,52 @@ glm::vec2 WorldMap::renderPosToMapUV(const glm::vec3& renderPos, int zoneIdx) co
|
||||||
return glm::vec2(u, v);
|
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
|
// Main render
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
|
|
@ -700,6 +778,11 @@ void WorldMap::render(const glm::vec3& playerRenderPos, int screenWidth, int scr
|
||||||
|
|
||||||
auto& input = core::Input::getInstance();
|
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)
|
// When map is open, always allow M/Escape to close (bypass ImGui keyboard capture)
|
||||||
if (open) {
|
if (open) {
|
||||||
if (input.isKeyJustPressed(SDL_SCANCODE_M) ||
|
if (input.isKeyJustPressed(SDL_SCANCODE_M) ||
|
||||||
|
|
@ -707,6 +790,14 @@ void WorldMap::render(const glm::vec3& playerRenderPos, int screenWidth, int scr
|
||||||
open = false;
|
open = false;
|
||||||
return;
|
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 {
|
} else {
|
||||||
auto& io = ImGui::GetIO();
|
auto& io = ImGui::GetIO();
|
||||||
if (!io.WantCaptureKeyboard && input.isKeyJustPressed(SDL_SCANCODE_M)) {
|
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;
|
compositedIdx = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure continent textures are loaded and composited
|
// Open directly to the player's current zone
|
||||||
if (continentIdx >= 0) {
|
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);
|
loadZoneTextures(continentIdx);
|
||||||
compositeZone(continentIdx);
|
compositeZone(continentIdx);
|
||||||
currentIdx = continentIdx;
|
currentIdx = continentIdx;
|
||||||
|
|
@ -942,17 +1040,25 @@ void WorldMap::renderImGuiOverlay(const glm::vec3& playerRenderPos, int screenWi
|
||||||
float sx1 = imgMin.x + zuMax * displayW;
|
float sx1 = imgMin.x + zuMax * displayW;
|
||||||
float sy1 = imgMin.y + zvMax * displayH;
|
float sy1 = imgMin.y + zvMax * displayH;
|
||||||
|
|
||||||
|
bool explored = exploredZones.count(zi) > 0;
|
||||||
|
|
||||||
// Check hover
|
// Check hover
|
||||||
bool hovered = (mousePos.x >= sx0 && mousePos.x <= sx1 &&
|
bool hovered = (mousePos.x >= sx0 && mousePos.x <= sx1 &&
|
||||||
mousePos.y >= sy0 && mousePos.y <= sy1);
|
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) {
|
if (hovered) {
|
||||||
hoveredZone = zi;
|
hoveredZone = zi;
|
||||||
drawList->AddRectFilled(ImVec2(sx0, sy0), ImVec2(sx1, sy1),
|
drawList->AddRectFilled(ImVec2(sx0, sy0), ImVec2(sx1, sy1),
|
||||||
IM_COL32(255, 255, 200, 40));
|
IM_COL32(255, 255, 200, 40));
|
||||||
drawList->AddRect(ImVec2(sx0, sy0), ImVec2(sx1, sy1),
|
drawList->AddRect(ImVec2(sx0, sy0), ImVec2(sx1, sy1),
|
||||||
IM_COL32(255, 215, 0, 180), 0.0f, 0, 2.0f);
|
IM_COL32(255, 215, 0, 180), 0.0f, 0, 2.0f);
|
||||||
} else {
|
} else if (explored) {
|
||||||
drawList->AddRect(ImVec2(sx0, sy0), ImVec2(sx1, sy1),
|
drawList->AddRect(ImVec2(sx0, sy0), ImVec2(sx1, sy1),
|
||||||
IM_COL32(255, 255, 255, 30), 0.0f, 0, 1.0f);
|
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
|
// Help text
|
||||||
const char* helpText;
|
const char* helpText;
|
||||||
if (viewLevel == ViewLevel::ZONE) {
|
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) {
|
} 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 {
|
} 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);
|
ImVec2 textSize = ImGui::CalcTextSize(helpText);
|
||||||
float textY = mapY + displayH + 8.0f;
|
float textY = mapY + displayH + 8.0f;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue