mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Fix mount sounds, grey WMO meshes, taxi landing, tree animations, and classic dismount
- Per-family mount sounds (kodo, tallstrider, mechanostrider, etc.) detected from M2 model path - Skip WMO groups with SHOW_SKYBOX flag or all-untextured batches (grey mesh in Orgrimmar) - Freeze physics during taxi landing until terrain loads to prevent falling through void - Disable bone animations on tropical vegetation (palm, bamboo, banana, etc.) to fix wiggling - Snap player to final taxi waypoint on flight completion - Extract mount aura spell ID from classic UNIT_FIELD_AURAS for CMSG_CANCEL_AURA dismount - Increase /unstuck forward nudge to 5 units
This commit is contained in:
parent
bf31da8c13
commit
d27387d744
13 changed files with 525 additions and 217 deletions
|
|
@ -12,6 +12,7 @@
|
|||
"UNIT_FIELD_FLAGS": 46,
|
||||
"UNIT_FIELD_DISPLAYID": 131,
|
||||
"UNIT_FIELD_MOUNTDISPLAYID": 133,
|
||||
"UNIT_FIELD_AURAS": 50,
|
||||
"UNIT_NPC_FLAGS": 147,
|
||||
"UNIT_DYNAMIC_FLAGS": 143,
|
||||
"UNIT_END": 188,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
"UNIT_FIELD_FLAGS": 46,
|
||||
"UNIT_FIELD_DISPLAYID": 131,
|
||||
"UNIT_FIELD_MOUNTDISPLAYID": 133,
|
||||
"UNIT_FIELD_AURAS": 50,
|
||||
"UNIT_NPC_FLAGS": 147,
|
||||
"UNIT_DYNAMIC_FLAGS": 143,
|
||||
"UNIT_END": 188,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
#include <string>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
#include <chrono>
|
||||
|
||||
namespace wowee {
|
||||
|
|
@ -24,7 +25,15 @@ enum class MountFamily {
|
|||
WOLF,
|
||||
TIGER,
|
||||
RAPTOR,
|
||||
DRAGON
|
||||
DRAGON,
|
||||
KODO,
|
||||
MECHANOSTRIDER,
|
||||
TALLSTRIDER,
|
||||
UNDEAD_HORSE
|
||||
};
|
||||
|
||||
struct MountFamilyHash {
|
||||
std::size_t operator()(MountFamily f) const { return static_cast<std::size_t>(f); }
|
||||
};
|
||||
|
||||
struct MountSample {
|
||||
|
|
@ -42,7 +51,7 @@ public:
|
|||
void update(float deltaTime);
|
||||
|
||||
// Called when mounting/dismounting
|
||||
void onMount(uint32_t creatureDisplayId, bool isFlying);
|
||||
void onMount(uint32_t creatureDisplayId, bool isFlying, const std::string& modelPath = "");
|
||||
void onDismount();
|
||||
|
||||
// Update movement state
|
||||
|
|
@ -63,6 +72,7 @@ public:
|
|||
private:
|
||||
MountType detectMountType(uint32_t creatureDisplayId) const;
|
||||
MountFamily detectMountFamily(uint32_t creatureDisplayId) const;
|
||||
MountFamily detectMountFamilyFromPath(const std::string& modelPath) const;
|
||||
void updateMountSounds();
|
||||
void stopAllMountSounds();
|
||||
void loadMountSounds();
|
||||
|
|
@ -80,11 +90,18 @@ private:
|
|||
// Mount sound samples (loaded from MPQ)
|
||||
std::vector<MountSample> wingFlapSounds_;
|
||||
std::vector<MountSample> wingIdleSounds_;
|
||||
std::vector<MountSample> horseBreathSounds_;
|
||||
std::vector<MountSample> horseMoveSounds_;
|
||||
std::vector<MountSample> horseJumpSounds_; // Jump effort sounds
|
||||
std::vector<MountSample> horseLandSounds_; // Landing thud sounds
|
||||
std::vector<MountSample> horseIdleSounds_; // Snorts and whinnies for idle
|
||||
|
||||
// Per-family ground mount sounds
|
||||
struct FamilySounds {
|
||||
std::vector<MountSample> move; // Movement ambient (alerts/whinnies/growls)
|
||||
std::vector<MountSample> jump; // Jump effort sounds
|
||||
std::vector<MountSample> land; // Landing wound/thud sounds
|
||||
std::vector<MountSample> idle; // Idle ambient (snorts/breathing/fidgets)
|
||||
};
|
||||
std::unordered_map<MountFamily, FamilySounds, MountFamilyHash> familySounds_;
|
||||
|
||||
// Helper to get sounds for current family (falls back to HORSE)
|
||||
const FamilySounds& getCurrentFamilySounds() const;
|
||||
|
||||
// Sound state tracking
|
||||
bool playingMovementSound_ = false;
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ enum class UF : uint16_t {
|
|||
UNIT_FIELD_FLAGS_2,
|
||||
UNIT_FIELD_DISPLAYID,
|
||||
UNIT_FIELD_MOUNTDISPLAYID,
|
||||
UNIT_FIELD_AURAS, // Start of aura spell ID array (48 consecutive uint32 slots, classic/vanilla only)
|
||||
UNIT_NPC_FLAGS,
|
||||
UNIT_DYNAMIC_FLAGS,
|
||||
UNIT_END,
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ public:
|
|||
void setEquippedWeaponType(uint32_t inventoryType) { equippedWeaponInvType_ = inventoryType; meleeAnimId = 0; }
|
||||
|
||||
// Mount rendering
|
||||
void setMounted(uint32_t mountInstId, uint32_t mountDisplayId, float heightOffset);
|
||||
void setMounted(uint32_t mountInstId, uint32_t mountDisplayId, float heightOffset, const std::string& modelPath = "");
|
||||
void setTaxiFlight(bool onTaxi) { taxiFlight_ = onTaxi; }
|
||||
void setMountPitchRoll(float pitch, float roll) { mountPitch_ = pitch; mountRoll_ = roll; }
|
||||
void clearMount();
|
||||
|
|
|
|||
|
|
@ -309,6 +309,7 @@ private:
|
|||
glm::vec3 boundingBoxMax;
|
||||
|
||||
uint32_t groupFlags = 0;
|
||||
bool allUntextured = false; // True if ALL batches use fallback white texture (collision/placeholder group)
|
||||
|
||||
// Material batches (start index, count, material ID)
|
||||
struct Batch {
|
||||
|
|
|
|||
|
|
@ -24,10 +24,13 @@ bool MountSoundManager::initialize(pipeline::AssetManager* assets) {
|
|||
|
||||
loadMountSounds();
|
||||
|
||||
int totalSamples = wingFlapSounds_.size() + wingIdleSounds_.size() +
|
||||
horseBreathSounds_.size() + horseMoveSounds_.size() +
|
||||
horseJumpSounds_.size() + horseLandSounds_.size();
|
||||
LOG_INFO("Mount sound manager initialized (", totalSamples, " clips)");
|
||||
int totalSamples = wingFlapSounds_.size() + wingIdleSounds_.size();
|
||||
for (const auto& [family, sounds] : familySounds_) {
|
||||
totalSamples += sounds.move.size() + sounds.jump.size() +
|
||||
sounds.land.size() + sounds.idle.size();
|
||||
}
|
||||
LOG_INFO("Mount sound manager initialized (", totalSamples, " clips, ",
|
||||
familySounds_.size(), " mount families)");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -36,11 +39,24 @@ void MountSoundManager::shutdown() {
|
|||
mounted_ = false;
|
||||
wingFlapSounds_.clear();
|
||||
wingIdleSounds_.clear();
|
||||
horseBreathSounds_.clear();
|
||||
horseMoveSounds_.clear();
|
||||
familySounds_.clear();
|
||||
assetManager_ = nullptr;
|
||||
}
|
||||
|
||||
static void loadSoundList(pipeline::AssetManager* assets,
|
||||
const std::vector<std::string>& paths,
|
||||
std::vector<MountSample>& out) {
|
||||
for (const auto& path : paths) {
|
||||
if (!assets->fileExists(path)) continue;
|
||||
auto data = assets->readFile(path);
|
||||
if (data.empty()) continue;
|
||||
MountSample sample;
|
||||
sample.path = path;
|
||||
sample.data = std::move(data);
|
||||
out.push_back(std::move(sample));
|
||||
}
|
||||
}
|
||||
|
||||
void MountSoundManager::loadMountSounds() {
|
||||
if (!assetManager_) return;
|
||||
|
||||
|
|
@ -52,7 +68,6 @@ void MountSoundManager::loadMountSounds() {
|
|||
"Sound\\Creature\\DragonWhelp\\mDragonWhelpWingFlapA.wav",
|
||||
"Sound\\Creature\\DragonWhelp\\mDragonWhelpWingFlapB.wav",
|
||||
};
|
||||
|
||||
for (const auto& path : wingFlapPaths) {
|
||||
MountSample sample;
|
||||
if (loadSound(path, sample)) {
|
||||
|
|
@ -60,13 +75,12 @@ void MountSoundManager::loadMountSounds() {
|
|||
}
|
||||
}
|
||||
|
||||
// Flying mount idle/hovering (screeches/calls)
|
||||
// Flying mount idle/hovering
|
||||
std::vector<std::string> wingIdlePaths = {
|
||||
"Sound\\Creature\\DragonHawk\\DragonHawkPreAggro.wav",
|
||||
"Sound\\Creature\\DragonHawk\\DragonHawkAggro.wav",
|
||||
"Sound\\Creature\\Dragons\\DragonPreAggro.wav",
|
||||
};
|
||||
|
||||
for (const auto& path : wingIdlePaths) {
|
||||
MountSample sample;
|
||||
if (loadSound(path, sample)) {
|
||||
|
|
@ -74,114 +88,247 @@ void MountSoundManager::loadMountSounds() {
|
|||
}
|
||||
}
|
||||
|
||||
// Ground mount breathing/idle (per creature family)
|
||||
std::vector<std::string> horseBreathPaths = {
|
||||
"Sound\\Creature\\Horse\\mHorseStand3A.wav",
|
||||
"Sound\\Creature\\Ram\\RamPreAggro.wav",
|
||||
"Sound\\Creature\\Wolf\\mWolfFidget2a.wav",
|
||||
"Sound\\Creature\\Tiger\\mTigerStand2A.wav",
|
||||
};
|
||||
// --- Per-family ground mount sounds ---
|
||||
|
||||
for (const auto& path : horseBreathPaths) {
|
||||
MountSample sample;
|
||||
if (loadSound(path, sample)) {
|
||||
horseBreathSounds_.push_back(std::move(sample));
|
||||
// Horse
|
||||
{
|
||||
auto& s = familySounds_[MountFamily::HORSE];
|
||||
loadSoundList(assetManager_, {
|
||||
"Sound\\Creature\\Horse\\mHorseAggroA.wav",
|
||||
}, s.move);
|
||||
loadSoundList(assetManager_, {
|
||||
"Sound\\Creature\\Horse\\mHorseAttackA.wav",
|
||||
"Sound\\Creature\\Horse\\mHorseAttackB.wav",
|
||||
"Sound\\Creature\\Horse\\mHorseAttackC.wav",
|
||||
}, s.jump);
|
||||
loadSoundList(assetManager_, {
|
||||
"Sound\\Creature\\Horse\\mHorseWoundA.wav",
|
||||
"Sound\\Creature\\Horse\\mHorseWoundB.wav",
|
||||
}, s.land);
|
||||
loadSoundList(assetManager_, {
|
||||
"Sound\\Creature\\Horse\\mHorseStand3A.wav",
|
||||
"Sound\\Creature\\Horse\\mHorseAggroA.wav",
|
||||
}, s.idle);
|
||||
}
|
||||
|
||||
// Wolf
|
||||
{
|
||||
auto& s = familySounds_[MountFamily::WOLF];
|
||||
loadSoundList(assetManager_, {
|
||||
"Sound\\Creature\\Wolf\\mWolfAggroA.wav",
|
||||
"Sound\\Creature\\Wolf\\mWolfAggroB.wav",
|
||||
"Sound\\Creature\\Wolf\\mWolfAggroC.wav",
|
||||
}, s.move);
|
||||
loadSoundList(assetManager_, {
|
||||
"Sound\\Creature\\Wolf\\mWolfAttackA.wav",
|
||||
"Sound\\Creature\\Wolf\\mWolfAttackB.wav",
|
||||
"Sound\\Creature\\Wolf\\mWolfAttackC.wav",
|
||||
}, s.jump);
|
||||
loadSoundList(assetManager_, {
|
||||
"Sound\\Creature\\Wolf\\mWolfWoundA.wav",
|
||||
"Sound\\Creature\\Wolf\\mWolfWoundB.wav",
|
||||
"Sound\\Creature\\Wolf\\mWolfWoundC.wav",
|
||||
}, s.land);
|
||||
loadSoundList(assetManager_, {
|
||||
"Sound\\Creature\\Wolf\\mWolfFidget2a.wav",
|
||||
"Sound\\Creature\\Wolf\\mWolfFidget2b.wav",
|
||||
"Sound\\Creature\\Wolf\\mWolfFidget2c.wav",
|
||||
}, s.idle);
|
||||
}
|
||||
|
||||
// Ram
|
||||
{
|
||||
auto& s = familySounds_[MountFamily::RAM];
|
||||
loadSoundList(assetManager_, {
|
||||
"Sound\\Creature\\Ram\\RamAggro.wav",
|
||||
"Sound\\Creature\\Ram\\RamPreAggro.wav",
|
||||
}, s.move);
|
||||
loadSoundList(assetManager_, {
|
||||
"Sound\\Creature\\Ram\\RamAttackA.wav",
|
||||
"Sound\\Creature\\Ram\\RamAttackB.wav",
|
||||
"Sound\\Creature\\Ram\\RamAttackC.wav",
|
||||
}, s.jump);
|
||||
loadSoundList(assetManager_, {
|
||||
"Sound\\Creature\\Ram\\RamWoundA.wav",
|
||||
"Sound\\Creature\\Ram\\RamWoundB.wav",
|
||||
"Sound\\Creature\\Ram\\RamWoundC.wav",
|
||||
}, s.land);
|
||||
loadSoundList(assetManager_, {
|
||||
"Sound\\Creature\\Ram\\RamPreAggro.wav",
|
||||
}, s.idle);
|
||||
}
|
||||
|
||||
// Tiger (also saber cats / nightsaber)
|
||||
{
|
||||
auto& s = familySounds_[MountFamily::TIGER];
|
||||
loadSoundList(assetManager_, {
|
||||
"Sound\\Creature\\Tiger\\mTigerAggroA.wav",
|
||||
}, s.move);
|
||||
loadSoundList(assetManager_, {
|
||||
"Sound\\Creature\\Tiger\\mTigerAttackA.wav",
|
||||
"Sound\\Creature\\Tiger\\mTigerAttackB.wav",
|
||||
"Sound\\Creature\\Tiger\\mTigerAttackC.wav",
|
||||
"Sound\\Creature\\Tiger\\mTigerAttackD.wav",
|
||||
}, s.jump);
|
||||
loadSoundList(assetManager_, {
|
||||
"Sound\\Creature\\Tiger\\mTigerWoundA.wav",
|
||||
"Sound\\Creature\\Tiger\\mTigerWoundB.wav",
|
||||
"Sound\\Creature\\Tiger\\mTigerWoundC.wav",
|
||||
}, s.land);
|
||||
loadSoundList(assetManager_, {
|
||||
"Sound\\Creature\\Tiger\\mTigerStand2A.wav",
|
||||
}, s.idle);
|
||||
}
|
||||
|
||||
// Raptor
|
||||
{
|
||||
auto& s = familySounds_[MountFamily::RAPTOR];
|
||||
loadSoundList(assetManager_, {
|
||||
"Sound\\Creature\\Raptor\\mRaptorAggroA.wav",
|
||||
}, s.move);
|
||||
loadSoundList(assetManager_, {
|
||||
"Sound\\Creature\\Raptor\\mRaptorAttackA.wav",
|
||||
"Sound\\Creature\\Raptor\\mRaptorAttackB.wav",
|
||||
"Sound\\Creature\\Raptor\\mRaptorAttackC.wav",
|
||||
}, s.jump);
|
||||
loadSoundList(assetManager_, {
|
||||
"Sound\\Creature\\Raptor\\mRaptorWoundA.wav",
|
||||
"Sound\\Creature\\Raptor\\mRaptorWoundB.wav",
|
||||
"Sound\\Creature\\Raptor\\mRaptorWoundC.wav",
|
||||
}, s.land);
|
||||
loadSoundList(assetManager_, {
|
||||
"Sound\\Creature\\Raptor\\mRaptorAggroA.wav",
|
||||
}, s.idle);
|
||||
}
|
||||
|
||||
// Kodo
|
||||
{
|
||||
auto& s = familySounds_[MountFamily::KODO];
|
||||
loadSoundList(assetManager_, {
|
||||
"Sound\\Creature\\KodoBeast\\KodoBeastAggroA.wav",
|
||||
}, s.move);
|
||||
loadSoundList(assetManager_, {
|
||||
"Sound\\Creature\\KodoBeast\\KodoBeastAttackA.wav",
|
||||
"Sound\\Creature\\KodoBeast\\KodoBeastAttackB.wav",
|
||||
"Sound\\Creature\\KodoBeast\\KodoBeastAttackC.wav",
|
||||
}, s.jump);
|
||||
loadSoundList(assetManager_, {
|
||||
"Sound\\Creature\\KodoBeast\\KodoBeastWoundA.wav",
|
||||
"Sound\\Creature\\KodoBeast\\KodoBeastWoundB.wav",
|
||||
"Sound\\Creature\\KodoBeast\\KodoBeastWoundC.wav",
|
||||
}, s.land);
|
||||
loadSoundList(assetManager_, {
|
||||
"Sound\\Creature\\KodoBeast\\KodoBeastStand02A.wav",
|
||||
}, s.idle);
|
||||
}
|
||||
|
||||
// Mechanostrider
|
||||
{
|
||||
auto& s = familySounds_[MountFamily::MECHANOSTRIDER];
|
||||
loadSoundList(assetManager_, {
|
||||
"Sound\\Creature\\MechaStrider\\MechaStriderAggro.wav",
|
||||
"Sound\\Creature\\MechaStrider\\MechaStriderPreAggro.wav",
|
||||
}, s.move);
|
||||
loadSoundList(assetManager_, {
|
||||
"Sound\\Creature\\MechaStrider\\MechaStriderAttackA.wav",
|
||||
"Sound\\Creature\\MechaStrider\\MechaStriderAttackB.wav",
|
||||
"Sound\\Creature\\MechaStrider\\MechaStriderAttackC.wav",
|
||||
}, s.jump);
|
||||
loadSoundList(assetManager_, {
|
||||
"Sound\\Creature\\MechaStrider\\MechaStriderAttackA.wav",
|
||||
}, s.land);
|
||||
loadSoundList(assetManager_, {
|
||||
"Sound\\Creature\\MechaStrider\\MechaStriderPreAggro.wav",
|
||||
}, s.idle);
|
||||
}
|
||||
|
||||
// Tallstrider (plainstrider mounts)
|
||||
{
|
||||
auto& s = familySounds_[MountFamily::TALLSTRIDER];
|
||||
loadSoundList(assetManager_, {
|
||||
"Sound\\Creature\\Tallstrider\\TallstriderAggro.wav",
|
||||
"Sound\\Creature\\Tallstrider\\TallstriderPreAggro.wav",
|
||||
}, s.move);
|
||||
loadSoundList(assetManager_, {
|
||||
"Sound\\Creature\\Tallstrider\\TallstriderAttackA.wav",
|
||||
"Sound\\Creature\\Tallstrider\\TallstriderAttackB.wav",
|
||||
"Sound\\Creature\\Tallstrider\\TallstriderAttackC.wav",
|
||||
}, s.jump);
|
||||
loadSoundList(assetManager_, {
|
||||
"Sound\\Creature\\Tallstrider\\TallstriderWoundA.wav",
|
||||
"Sound\\Creature\\Tallstrider\\TallstriderWoundB.wav",
|
||||
"Sound\\Creature\\Tallstrider\\TallstriderWoundC.wav",
|
||||
}, s.land);
|
||||
loadSoundList(assetManager_, {
|
||||
"Sound\\Creature\\Tallstrider\\TallstriderPreAggro.wav",
|
||||
}, s.idle);
|
||||
}
|
||||
|
||||
// Undead horse (skeletal warhorse)
|
||||
{
|
||||
auto& s = familySounds_[MountFamily::UNDEAD_HORSE];
|
||||
loadSoundList(assetManager_, {
|
||||
"Sound\\Creature\\HorseUndead\\HorseUndeadAggro.wav",
|
||||
}, s.move);
|
||||
loadSoundList(assetManager_, {
|
||||
"Sound\\Creature\\HorseUndead\\HorseUndeadAttackA.wav",
|
||||
"Sound\\Creature\\HorseUndead\\HorseUndeadAttackB.wav",
|
||||
"Sound\\Creature\\HorseUndead\\HorseUndeadAttackC.wav",
|
||||
}, s.jump);
|
||||
loadSoundList(assetManager_, {
|
||||
"Sound\\Creature\\HorseUndead\\HorseUndeadWoundA.wav",
|
||||
"Sound\\Creature\\HorseUndead\\HorseUndeadWoundB.wav",
|
||||
"Sound\\Creature\\HorseUndead\\HorseUndeadWoundC.wav",
|
||||
}, s.land);
|
||||
loadSoundList(assetManager_, {
|
||||
"Sound\\Creature\\HorseUndead\\HorseUndeadPreAggro.wav",
|
||||
}, s.idle);
|
||||
}
|
||||
|
||||
// Log loaded families
|
||||
for (const auto& [family, sounds] : familySounds_) {
|
||||
int total = sounds.move.size() + sounds.jump.size() +
|
||||
sounds.land.size() + sounds.idle.size();
|
||||
if (total > 0) {
|
||||
LOG_INFO("Mount family ", static_cast<int>(family), ": ",
|
||||
sounds.move.size(), " move, ", sounds.jump.size(), " jump, ",
|
||||
sounds.land.size(), " land, ", sounds.idle.size(), " idle");
|
||||
}
|
||||
}
|
||||
|
||||
// Ground mount movement ambient (alerts/whinnies)
|
||||
std::vector<std::string> horseMovePaths = {
|
||||
"Sound\\Creature\\Horse\\mHorseAggroA.wav",
|
||||
};
|
||||
|
||||
for (const auto& path : horseMovePaths) {
|
||||
MountSample sample;
|
||||
if (loadSound(path, sample)) {
|
||||
horseMoveSounds_.push_back(std::move(sample));
|
||||
}
|
||||
}
|
||||
|
||||
// Ground mount jump effort sounds
|
||||
std::vector<std::string> horseJumpPaths = {
|
||||
"Sound\\Creature\\Horse\\mHorseAttackA.wav",
|
||||
"Sound\\Creature\\Horse\\mHorseAttackB.wav",
|
||||
"Sound\\Creature\\Horse\\mHorseAttackC.wav",
|
||||
};
|
||||
|
||||
for (const auto& path : horseJumpPaths) {
|
||||
MountSample sample;
|
||||
if (loadSound(path, sample)) {
|
||||
horseJumpSounds_.push_back(std::move(sample));
|
||||
}
|
||||
}
|
||||
|
||||
// Ground mount landing thud sounds
|
||||
std::vector<std::string> horseLandPaths = {
|
||||
"Sound\\Creature\\Horse\\mHorseWoundA.wav",
|
||||
"Sound\\Creature\\Horse\\mHorseWoundB.wav",
|
||||
};
|
||||
|
||||
for (const auto& path : horseLandPaths) {
|
||||
MountSample sample;
|
||||
if (loadSound(path, sample)) {
|
||||
horseLandSounds_.push_back(std::move(sample));
|
||||
}
|
||||
}
|
||||
|
||||
// Ground mount idle ambient (snorts and whinnies only)
|
||||
std::vector<std::string> horseIdlePaths = {
|
||||
"Sound\\Creature\\Horse\\mHorseStand3A.wav", // Snort
|
||||
"Sound\\Creature\\Horse\\mHorseAggroA.wav", // Whinny
|
||||
};
|
||||
|
||||
for (const auto& path : horseIdlePaths) {
|
||||
MountSample sample;
|
||||
if (loadSound(path, sample)) {
|
||||
horseIdleSounds_.push_back(std::move(sample));
|
||||
}
|
||||
}
|
||||
|
||||
if (!wingFlapSounds_.empty()) {
|
||||
LOG_INFO("Loaded ", wingFlapSounds_.size(), " wing flap sounds");
|
||||
}
|
||||
if (!wingIdleSounds_.empty()) {
|
||||
LOG_INFO("Loaded ", wingIdleSounds_.size(), " wing idle sounds");
|
||||
}
|
||||
if (!horseBreathSounds_.empty()) {
|
||||
LOG_INFO("Loaded ", horseBreathSounds_.size(), " horse breath sounds");
|
||||
}
|
||||
if (!horseMoveSounds_.empty()) {
|
||||
LOG_INFO("Loaded ", horseMoveSounds_.size(), " horse move sounds");
|
||||
}
|
||||
if (!horseJumpSounds_.empty()) {
|
||||
LOG_INFO("Loaded ", horseJumpSounds_.size(), " horse jump sounds");
|
||||
}
|
||||
if (!horseLandSounds_.empty()) {
|
||||
LOG_INFO("Loaded ", horseLandSounds_.size(), " horse land sounds");
|
||||
}
|
||||
if (!horseIdleSounds_.empty()) {
|
||||
LOG_INFO("Loaded ", horseIdleSounds_.size(), " horse idle sounds (snorts/whinnies)");
|
||||
}
|
||||
}
|
||||
|
||||
bool MountSoundManager::loadSound(const std::string& path, MountSample& sample) {
|
||||
if (!assetManager_ || !assetManager_->fileExists(path)) {
|
||||
LOG_WARNING("Mount sound file not found: ", path);
|
||||
return false;
|
||||
}
|
||||
|
||||
auto data = assetManager_->readFile(path);
|
||||
if (data.empty()) {
|
||||
LOG_WARNING("Mount sound file empty: ", path);
|
||||
return false;
|
||||
}
|
||||
|
||||
sample.path = path;
|
||||
sample.data = std::move(data);
|
||||
LOG_INFO("Loaded mount sound: ", path);
|
||||
return true;
|
||||
}
|
||||
|
||||
const MountSoundManager::FamilySounds& MountSoundManager::getCurrentFamilySounds() const {
|
||||
static const FamilySounds empty;
|
||||
|
||||
auto it = familySounds_.find(currentMountFamily_);
|
||||
if (it != familySounds_.end()) {
|
||||
return it->second;
|
||||
}
|
||||
|
||||
// Fall back to horse for unknown families
|
||||
it = familySounds_.find(MountFamily::HORSE);
|
||||
if (it != familySounds_.end()) {
|
||||
return it->second;
|
||||
}
|
||||
|
||||
return empty;
|
||||
}
|
||||
|
||||
void MountSoundManager::update(float deltaTime) {
|
||||
if (!mounted_) {
|
||||
soundLoopTimer_ = 0.0f;
|
||||
|
|
@ -192,18 +339,26 @@ void MountSoundManager::update(float deltaTime) {
|
|||
updateMountSounds();
|
||||
}
|
||||
|
||||
void MountSoundManager::onMount(uint32_t creatureDisplayId, bool isFlying) {
|
||||
void MountSoundManager::onMount(uint32_t creatureDisplayId, bool isFlying, const std::string& modelPath) {
|
||||
mounted_ = true;
|
||||
currentDisplayId_ = creatureDisplayId;
|
||||
currentMountType_ = detectMountType(creatureDisplayId);
|
||||
currentMountFamily_ = detectMountFamily(creatureDisplayId);
|
||||
|
||||
// Prefer model path detection (reliable) over display ID ranges (fragile)
|
||||
if (!modelPath.empty()) {
|
||||
currentMountFamily_ = detectMountFamilyFromPath(modelPath);
|
||||
} else {
|
||||
currentMountFamily_ = detectMountFamily(creatureDisplayId);
|
||||
}
|
||||
|
||||
flying_ = isFlying;
|
||||
moving_ = false;
|
||||
|
||||
LOG_INFO("Mount sound: mounted on display ID ", creatureDisplayId,
|
||||
" type=", static_cast<int>(currentMountType_),
|
||||
" family=", static_cast<int>(currentMountFamily_),
|
||||
" flying=", flying_);
|
||||
" flying=", flying_,
|
||||
" model=", modelPath);
|
||||
|
||||
updateMountSounds();
|
||||
}
|
||||
|
|
@ -212,6 +367,7 @@ void MountSoundManager::onDismount() {
|
|||
stopAllMountSounds();
|
||||
mounted_ = false;
|
||||
currentMountType_ = MountType::NONE;
|
||||
currentMountFamily_ = MountFamily::UNKNOWN;
|
||||
currentDisplayId_ = 0;
|
||||
flying_ = false;
|
||||
moving_ = false;
|
||||
|
|
@ -242,23 +398,22 @@ void MountSoundManager::setGrounded(bool grounded) {
|
|||
void MountSoundManager::playRearUpSound() {
|
||||
if (!mounted_) return;
|
||||
|
||||
// Cooldown to prevent spam (200ms)
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(now - lastActionSoundTime_).count();
|
||||
if (elapsed < 200) return;
|
||||
lastActionSoundTime_ = now;
|
||||
|
||||
// Use semantic sound based on mount family
|
||||
if (currentMountType_ == MountType::GROUND && !horseMoveSounds_.empty()) {
|
||||
// Ground mounts: whinny/roar
|
||||
static std::mt19937 rng(std::random_device{}());
|
||||
std::uniform_int_distribution<size_t> dist(0, horseMoveSounds_.size() - 1);
|
||||
const auto& sample = horseMoveSounds_[dist(rng)];
|
||||
if (!sample.data.empty()) {
|
||||
AudioEngine::instance().playSound2D(sample.data, 0.7f * volumeScale_, 1.0f);
|
||||
if (currentMountType_ == MountType::GROUND) {
|
||||
const auto& sounds = getCurrentFamilySounds();
|
||||
if (!sounds.move.empty()) {
|
||||
static std::mt19937 rng(std::random_device{}());
|
||||
std::uniform_int_distribution<size_t> dist(0, sounds.move.size() - 1);
|
||||
const auto& sample = sounds.move[dist(rng)];
|
||||
if (!sample.data.empty()) {
|
||||
AudioEngine::instance().playSound2D(sample.data, 0.7f * volumeScale_, 1.0f);
|
||||
}
|
||||
}
|
||||
} else if (currentMountType_ == MountType::FLYING && !wingIdleSounds_.empty()) {
|
||||
// Flying mounts: screech/roar
|
||||
static std::mt19937 rng(std::random_device{}());
|
||||
std::uniform_int_distribution<size_t> dist(0, wingIdleSounds_.size() - 1);
|
||||
const auto& sample = wingIdleSounds_[dist(rng)];
|
||||
|
|
@ -271,22 +426,22 @@ void MountSoundManager::playRearUpSound() {
|
|||
void MountSoundManager::playJumpSound() {
|
||||
if (!mounted_) return;
|
||||
|
||||
// Cooldown to prevent spam
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(now - lastActionSoundTime_).count();
|
||||
if (elapsed < 200) return;
|
||||
lastActionSoundTime_ = now;
|
||||
|
||||
// Jump effort sound
|
||||
if (currentMountType_ == MountType::GROUND && !horseJumpSounds_.empty()) {
|
||||
static std::mt19937 rng(std::random_device{}());
|
||||
std::uniform_int_distribution<size_t> dist(0, horseJumpSounds_.size() - 1);
|
||||
const auto& sample = horseJumpSounds_[dist(rng)];
|
||||
if (!sample.data.empty()) {
|
||||
AudioEngine::instance().playSound2D(sample.data, 0.5f * volumeScale_, 1.1f);
|
||||
if (currentMountType_ == MountType::GROUND) {
|
||||
const auto& sounds = getCurrentFamilySounds();
|
||||
if (!sounds.jump.empty()) {
|
||||
static std::mt19937 rng(std::random_device{}());
|
||||
std::uniform_int_distribution<size_t> dist(0, sounds.jump.size() - 1);
|
||||
const auto& sample = sounds.jump[dist(rng)];
|
||||
if (!sample.data.empty()) {
|
||||
AudioEngine::instance().playSound2D(sample.data, 0.5f * volumeScale_, 1.1f);
|
||||
}
|
||||
}
|
||||
} else if (currentMountType_ == MountType::FLYING && !wingFlapSounds_.empty()) {
|
||||
// Flying mounts: wing whoosh
|
||||
static std::mt19937 rng(std::random_device{}());
|
||||
std::uniform_int_distribution<size_t> dist(0, wingFlapSounds_.size() - 1);
|
||||
const auto& sample = wingFlapSounds_[dist(rng)];
|
||||
|
|
@ -299,19 +454,20 @@ void MountSoundManager::playJumpSound() {
|
|||
void MountSoundManager::playLandSound() {
|
||||
if (!mounted_) return;
|
||||
|
||||
// Cooldown to prevent spam
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(now - lastActionSoundTime_).count();
|
||||
if (elapsed < 200) return;
|
||||
lastActionSoundTime_ = now;
|
||||
|
||||
// Landing thud/hoof sound
|
||||
if (currentMountType_ == MountType::GROUND && !horseLandSounds_.empty()) {
|
||||
static std::mt19937 rng(std::random_device{}());
|
||||
std::uniform_int_distribution<size_t> dist(0, horseLandSounds_.size() - 1);
|
||||
const auto& sample = horseLandSounds_[dist(rng)];
|
||||
if (!sample.data.empty()) {
|
||||
AudioEngine::instance().playSound2D(sample.data, 0.6f * volumeScale_, 0.85f);
|
||||
if (currentMountType_ == MountType::GROUND) {
|
||||
const auto& sounds = getCurrentFamilySounds();
|
||||
if (!sounds.land.empty()) {
|
||||
static std::mt19937 rng(std::random_device{}());
|
||||
std::uniform_int_distribution<size_t> dist(0, sounds.land.size() - 1);
|
||||
const auto& sample = sounds.land[dist(rng)];
|
||||
if (!sample.data.empty()) {
|
||||
AudioEngine::instance().playSound2D(sample.data, 0.6f * volumeScale_, 0.85f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -319,62 +475,137 @@ void MountSoundManager::playLandSound() {
|
|||
void MountSoundManager::playIdleSound() {
|
||||
if (!mounted_ || moving_) return;
|
||||
|
||||
// Ambient idle sounds (snorts and whinnies only for ground mounts)
|
||||
if (currentMountType_ == MountType::GROUND && !horseIdleSounds_.empty()) {
|
||||
static std::mt19937 rng(std::random_device{}());
|
||||
std::uniform_int_distribution<size_t> dist(0, horseIdleSounds_.size() - 1);
|
||||
const auto& sample = horseIdleSounds_[dist(rng)];
|
||||
if (!sample.data.empty()) {
|
||||
AudioEngine::instance().playSound2D(sample.data, 0.35f * volumeScale_, 0.95f);
|
||||
if (currentMountType_ == MountType::GROUND) {
|
||||
const auto& sounds = getCurrentFamilySounds();
|
||||
if (!sounds.idle.empty()) {
|
||||
static std::mt19937 rng(std::random_device{}());
|
||||
std::uniform_int_distribution<size_t> dist(0, sounds.idle.size() - 1);
|
||||
const auto& sample = sounds.idle[dist(rng)];
|
||||
if (!sample.data.empty()) {
|
||||
AudioEngine::instance().playSound2D(sample.data, 0.35f * volumeScale_, 0.95f);
|
||||
}
|
||||
}
|
||||
}
|
||||
// No idle sounds for flying mounts (wing sounds were too aggressive)
|
||||
}
|
||||
|
||||
MountType MountSoundManager::detectMountType(uint32_t creatureDisplayId) const {
|
||||
// TODO: Load from CreatureDisplayInfo.dbc or CreatureModelData.dbc
|
||||
// For now, use simple heuristics based on common display IDs
|
||||
// This is a placeholder - we'd need proper DBC parsing for accuracy
|
||||
|
||||
// Common flying mount display IDs (approximate ranges)
|
||||
// Gryphons: ~2300-2310
|
||||
// Wyverns: ~1566-1570
|
||||
// Drakes: ~25830-25870
|
||||
// Phoenixes: ~17890-17900
|
||||
|
||||
if (creatureDisplayId >= 2300 && creatureDisplayId <= 2320) return MountType::FLYING; // Gryphons
|
||||
if (creatureDisplayId >= 1560 && creatureDisplayId <= 1580) return MountType::FLYING; // Wyverns
|
||||
if (creatureDisplayId >= 25800 && creatureDisplayId <= 25900) return MountType::FLYING; // Drakes
|
||||
if (creatureDisplayId >= 17880 && creatureDisplayId <= 17910) return MountType::FLYING; // Phoenixes
|
||||
|
||||
// Most other mounts are ground
|
||||
return MountType::GROUND;
|
||||
}
|
||||
|
||||
MountFamily MountSoundManager::detectMountFamilyFromPath(const std::string& modelPath) const {
|
||||
// Convert path to lowercase for matching
|
||||
std::string lower = modelPath;
|
||||
for (char& c : lower) c = std::tolower(c);
|
||||
|
||||
// Check creature model path for family keywords
|
||||
if (lower.find("tallstrider") != std::string::npos ||
|
||||
lower.find("plainstrider") != std::string::npos)
|
||||
return MountFamily::TALLSTRIDER;
|
||||
if (lower.find("wolf") != std::string::npos ||
|
||||
lower.find("direwolf") != std::string::npos)
|
||||
return MountFamily::WOLF;
|
||||
if (lower.find("tiger") != std::string::npos ||
|
||||
lower.find("nightsaber") != std::string::npos ||
|
||||
lower.find("panther") != std::string::npos ||
|
||||
lower.find("frostsaber") != std::string::npos)
|
||||
return MountFamily::TIGER;
|
||||
if (lower.find("raptor") != std::string::npos)
|
||||
return MountFamily::RAPTOR;
|
||||
if (lower.find("kodo") != std::string::npos)
|
||||
return MountFamily::KODO;
|
||||
if (lower.find("mechastrider") != std::string::npos ||
|
||||
lower.find("mechanostrider") != std::string::npos)
|
||||
return MountFamily::MECHANOSTRIDER;
|
||||
if (lower.find("ram") != std::string::npos)
|
||||
return MountFamily::RAM;
|
||||
if (lower.find("horseundead") != std::string::npos ||
|
||||
lower.find("skeletalhorse") != std::string::npos)
|
||||
return MountFamily::UNDEAD_HORSE;
|
||||
if (lower.find("horse") != std::string::npos)
|
||||
return MountFamily::HORSE;
|
||||
if (lower.find("hawkstrider") != std::string::npos)
|
||||
return MountFamily::TALLSTRIDER; // BC hawkstriders share tallstrider sounds
|
||||
if (lower.find("dragon") != std::string::npos ||
|
||||
lower.find("drake") != std::string::npos ||
|
||||
lower.find("gryphon") != std::string::npos ||
|
||||
lower.find("wyvern") != std::string::npos)
|
||||
return MountFamily::DRAGON;
|
||||
|
||||
return MountFamily::HORSE; // Default fallback
|
||||
}
|
||||
|
||||
MountFamily MountSoundManager::detectMountFamily(uint32_t creatureDisplayId) const {
|
||||
// Heuristic creature family detection based on common display ID ranges
|
||||
// TODO: Replace with proper CreatureModelData.dbc lookup
|
||||
// Undead horses (skeletal warhorses) — check before generic horse range
|
||||
// Display IDs: 10670 (Skeletal Horse), 10671-10672, 5765-5768
|
||||
if ((creatureDisplayId >= 10670 && creatureDisplayId <= 10672) ||
|
||||
(creatureDisplayId >= 5765 && creatureDisplayId <= 5768))
|
||||
return MountFamily::UNDEAD_HORSE;
|
||||
|
||||
// Horses: ~14000-14999 range (includes many horse variants)
|
||||
if (creatureDisplayId >= 14000 && creatureDisplayId < 15000) return MountFamily::HORSE;
|
||||
// Horses: common palomino/pinto/black stallion etc.
|
||||
// 2402-2410 (standard horses), 14337-14348 (epic horses)
|
||||
if ((creatureDisplayId >= 2402 && creatureDisplayId <= 2410) ||
|
||||
(creatureDisplayId >= 14337 && creatureDisplayId <= 14348) ||
|
||||
creatureDisplayId == 2404 || creatureDisplayId == 2405)
|
||||
return MountFamily::HORSE;
|
||||
|
||||
// Rams: ~14349-14375 range
|
||||
if (creatureDisplayId >= 14349 && creatureDisplayId <= 14375) return MountFamily::RAM;
|
||||
// Rams: Dwarf ram mounts
|
||||
// 2736-2737 (standard rams), 14347-14353 (epic rams), 14577-14578
|
||||
if ((creatureDisplayId >= 2736 && creatureDisplayId <= 2737) ||
|
||||
(creatureDisplayId >= 14347 && creatureDisplayId <= 14353) ||
|
||||
(creatureDisplayId >= 14577 && creatureDisplayId <= 14578))
|
||||
return MountFamily::RAM;
|
||||
|
||||
// Wolves: ~207-217, ~2326-2329 ranges
|
||||
if ((creatureDisplayId >= 207 && creatureDisplayId <= 217) ||
|
||||
(creatureDisplayId >= 2326 && creatureDisplayId <= 2329)) return MountFamily::WOLF;
|
||||
// Wolves: Orc wolf mounts
|
||||
// 207-212 (dire wolves), 2320-2328 (epic wolves), 14573-14575
|
||||
if ((creatureDisplayId >= 207 && creatureDisplayId <= 212) ||
|
||||
(creatureDisplayId >= 2320 && creatureDisplayId <= 2330) ||
|
||||
(creatureDisplayId >= 14573 && creatureDisplayId <= 14575))
|
||||
return MountFamily::WOLF;
|
||||
|
||||
// Tigers/Cats: ~6442-6473 range
|
||||
if (creatureDisplayId >= 6442 && creatureDisplayId <= 6473) return MountFamily::TIGER;
|
||||
// Tigers/Nightsabers: Night elf cat mounts
|
||||
// 6068-6074 (nightsabers), 6075-6080 (epic/frostsaber), 9991-9992, 14328-14332
|
||||
if ((creatureDisplayId >= 6068 && creatureDisplayId <= 6080) ||
|
||||
(creatureDisplayId >= 9991 && creatureDisplayId <= 9992) ||
|
||||
(creatureDisplayId >= 14328 && creatureDisplayId <= 14332))
|
||||
return MountFamily::TIGER;
|
||||
|
||||
// Raptors: ~6466-6474 range
|
||||
if (creatureDisplayId >= 6466 && creatureDisplayId <= 6474) return MountFamily::RAPTOR;
|
||||
// Raptors: Troll raptor mounts
|
||||
// 6469-6474 (standard raptors), 14338-14344 (epic raptors), 4806
|
||||
if ((creatureDisplayId >= 6469 && creatureDisplayId <= 6474) ||
|
||||
(creatureDisplayId >= 14338 && creatureDisplayId <= 14344) ||
|
||||
creatureDisplayId == 4806)
|
||||
return MountFamily::RAPTOR;
|
||||
|
||||
// Dragons/Drakes
|
||||
if (creatureDisplayId >= 25800 && creatureDisplayId <= 25900) return MountFamily::DRAGON;
|
||||
// Kodo: Tauren kodo mounts
|
||||
// 11641-11643 (standard kodo), 14348-14349 (great kodo), 12246
|
||||
if ((creatureDisplayId >= 11641 && creatureDisplayId <= 11643) ||
|
||||
(creatureDisplayId >= 14348 && creatureDisplayId <= 14349) ||
|
||||
creatureDisplayId == 12246 || creatureDisplayId == 14349)
|
||||
return MountFamily::KODO;
|
||||
|
||||
// Default to horse for unknown ground mounts
|
||||
// Mechanostriders: Gnome mechanostrider mounts
|
||||
// 6569-6571 (standard), 9473-9476 (epic), 10180, 14584
|
||||
if ((creatureDisplayId >= 6569 && creatureDisplayId <= 6571) ||
|
||||
(creatureDisplayId >= 9473 && creatureDisplayId <= 9476) ||
|
||||
creatureDisplayId == 10180 || creatureDisplayId == 14584)
|
||||
return MountFamily::MECHANOSTRIDER;
|
||||
|
||||
// Tallstriders (plainstrider mounts — Turtle WoW custom or future)
|
||||
// 1961-1964
|
||||
if (creatureDisplayId >= 1961 && creatureDisplayId <= 1964)
|
||||
return MountFamily::TALLSTRIDER;
|
||||
|
||||
// Dragons/Drakes (flying)
|
||||
if (creatureDisplayId >= 25800 && creatureDisplayId <= 25900)
|
||||
return MountFamily::DRAGON;
|
||||
|
||||
// Default to HORSE for unknown ground mounts (safe fallback)
|
||||
return MountFamily::HORSE;
|
||||
}
|
||||
|
||||
|
|
@ -388,66 +619,51 @@ void MountSoundManager::updateMountSounds() {
|
|||
// Flying mounts
|
||||
if (currentMountType_ == MountType::FLYING && flying_) {
|
||||
if (moving_ && !wingFlapSounds_.empty()) {
|
||||
// Wing flaps when moving (play periodically for continuous flapping sound)
|
||||
if (soundLoopTimer_ >= 1.2f) {
|
||||
std::uniform_int_distribution<size_t> dist(0, wingFlapSounds_.size() - 1);
|
||||
const auto& sample = wingFlapSounds_[dist(rng)];
|
||||
std::uniform_real_distribution<float> volumeDist(0.4f, 0.5f);
|
||||
std::uniform_real_distribution<float> pitchDist(0.95f, 1.05f);
|
||||
AudioEngine::instance().playSound2D(
|
||||
sample.data,
|
||||
volumeDist(rng) * volumeScale_,
|
||||
pitchDist(rng)
|
||||
);
|
||||
sample.data, volumeDist(rng) * volumeScale_, pitchDist(rng));
|
||||
soundLoopTimer_ = 0.0f;
|
||||
playingMovementSound_ = true;
|
||||
}
|
||||
} else if (!moving_ && !wingIdleSounds_.empty()) {
|
||||
// Idle/hovering sounds (less frequent)
|
||||
if (soundLoopTimer_ >= 3.5f) {
|
||||
std::uniform_int_distribution<size_t> dist(0, wingIdleSounds_.size() - 1);
|
||||
const auto& sample = wingIdleSounds_[dist(rng)];
|
||||
std::uniform_real_distribution<float> volumeDist(0.3f, 0.4f);
|
||||
std::uniform_real_distribution<float> pitchDist(0.98f, 1.02f);
|
||||
AudioEngine::instance().playSound2D(
|
||||
sample.data,
|
||||
volumeDist(rng) * volumeScale_,
|
||||
pitchDist(rng)
|
||||
);
|
||||
sample.data, volumeDist(rng) * volumeScale_, pitchDist(rng));
|
||||
soundLoopTimer_ = 0.0f;
|
||||
playingIdleSound_ = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Ground mounts
|
||||
// Ground mounts — use per-family sounds
|
||||
else if (currentMountType_ == MountType::GROUND && !flying_) {
|
||||
if (moving_ && !horseMoveSounds_.empty()) {
|
||||
// Occasional whinny/ambient sounds while moving
|
||||
const auto& sounds = getCurrentFamilySounds();
|
||||
if (moving_ && !sounds.move.empty()) {
|
||||
if (soundLoopTimer_ >= 8.0f) {
|
||||
std::uniform_int_distribution<size_t> dist(0, horseMoveSounds_.size() - 1);
|
||||
const auto& sample = horseMoveSounds_[dist(rng)];
|
||||
std::uniform_int_distribution<size_t> dist(0, sounds.move.size() - 1);
|
||||
const auto& sample = sounds.move[dist(rng)];
|
||||
std::uniform_real_distribution<float> volumeDist(0.35f, 0.45f);
|
||||
std::uniform_real_distribution<float> pitchDist(0.97f, 1.03f);
|
||||
AudioEngine::instance().playSound2D(
|
||||
sample.data,
|
||||
volumeDist(rng) * volumeScale_,
|
||||
pitchDist(rng)
|
||||
);
|
||||
sample.data, volumeDist(rng) * volumeScale_, pitchDist(rng));
|
||||
soundLoopTimer_ = 0.0f;
|
||||
playingMovementSound_ = true;
|
||||
}
|
||||
} else if (!moving_ && !horseBreathSounds_.empty()) {
|
||||
// Breathing/snorting when idle
|
||||
} else if (!moving_ && !sounds.idle.empty()) {
|
||||
if (soundLoopTimer_ >= 4.5f) {
|
||||
std::uniform_int_distribution<size_t> dist(0, horseBreathSounds_.size() - 1);
|
||||
const auto& sample = horseBreathSounds_[dist(rng)];
|
||||
std::uniform_int_distribution<size_t> dist(0, sounds.idle.size() - 1);
|
||||
const auto& sample = sounds.idle[dist(rng)];
|
||||
std::uniform_real_distribution<float> volumeDist(0.25f, 0.35f);
|
||||
std::uniform_real_distribution<float> pitchDist(0.98f, 1.02f);
|
||||
AudioEngine::instance().playSound2D(
|
||||
sample.data,
|
||||
volumeDist(rng) * volumeScale_,
|
||||
pitchDist(rng)
|
||||
);
|
||||
sample.data, volumeDist(rng) * volumeScale_, pitchDist(rng));
|
||||
soundLoopTimer_ = 0.0f;
|
||||
playingIdleSound_ = true;
|
||||
}
|
||||
|
|
@ -456,7 +672,6 @@ void MountSoundManager::updateMountSounds() {
|
|||
}
|
||||
|
||||
void MountSoundManager::stopAllMountSounds() {
|
||||
// Reset state flags
|
||||
playingMovementSound_ = false;
|
||||
playingIdleSound_ = false;
|
||||
soundLoopTimer_ = 0.0f;
|
||||
|
|
|
|||
|
|
@ -712,7 +712,12 @@ void Application::update(float deltaTime) {
|
|||
}
|
||||
if (renderer && renderer->getCameraController()) {
|
||||
const bool externallyDrivenMotion = onTaxi || onTransportNow;
|
||||
renderer->getCameraController()->setExternalFollow(externallyDrivenMotion);
|
||||
// Keep physics frozen (externalFollow) during landing clamp when terrain
|
||||
// hasn't loaded yet — prevents gravity from pulling player through void.
|
||||
bool landingClampActive = !onTaxi && taxiLandingClampTimer_ > 0.0f &&
|
||||
worldEntryMovementGraceTimer_ <= 0.0f &&
|
||||
!gameHandler->isMounted();
|
||||
renderer->getCameraController()->setExternalFollow(externallyDrivenMotion || landingClampActive);
|
||||
renderer->getCameraController()->setExternalMoving(externallyDrivenMotion);
|
||||
if (externallyDrivenMotion) {
|
||||
// Drop any stale local movement toggles while server drives taxi motion.
|
||||
|
|
@ -721,15 +726,11 @@ void Application::update(float deltaTime) {
|
|||
}
|
||||
if (lastTaxiFlight_ && !onTaxi) {
|
||||
renderer->getCameraController()->clearMovementInputs();
|
||||
// Keep clamping for a short grace window after taxi ends to avoid
|
||||
// falling through WMOs while floor/collision state settles.
|
||||
taxiLandingClampTimer_ = 1.5f;
|
||||
// Keep clamping until terrain loads at landing position.
|
||||
// Timer only counts down once a valid floor is found.
|
||||
taxiLandingClampTimer_ = 2.0f;
|
||||
}
|
||||
if (!onTaxi &&
|
||||
worldEntryMovementGraceTimer_ <= 0.0f &&
|
||||
!gameHandler->isMounted() &&
|
||||
taxiLandingClampTimer_ > 0.0f) {
|
||||
taxiLandingClampTimer_ -= deltaTime;
|
||||
if (landingClampActive) {
|
||||
if (renderer && gameHandler) {
|
||||
glm::vec3 p = renderer->getCharacterPosition();
|
||||
std::optional<float> terrainFloor;
|
||||
|
|
@ -753,17 +754,18 @@ void Application::update(float deltaTime) {
|
|||
if (m2Floor && (!targetFloor || *m2Floor > *targetFloor)) targetFloor = m2Floor;
|
||||
|
||||
if (targetFloor) {
|
||||
// Floor found — snap player to it and start countdown to release
|
||||
float targetZ = *targetFloor + 0.10f;
|
||||
// Only lift upward to prevent sinking through floors/bridges.
|
||||
// Never force the player downward from a valid elevated surface.
|
||||
if (p.z < targetZ - 0.05f) {
|
||||
if (std::abs(p.z - targetZ) > 0.05f) {
|
||||
p.z = targetZ;
|
||||
renderer->getCharacterPosition() = p;
|
||||
glm::vec3 canonical = core::coords::renderToCanonical(p);
|
||||
gameHandler->setPosition(canonical.x, canonical.y, canonical.z);
|
||||
gameHandler->sendMovement(game::Opcode::CMSG_MOVE_HEARTBEAT);
|
||||
}
|
||||
taxiLandingClampTimer_ -= deltaTime;
|
||||
}
|
||||
// No floor found: don't decrement timer, keep player frozen until terrain loads
|
||||
}
|
||||
}
|
||||
bool idleOrbit = renderer->getCameraController()->isIdleOrbit();
|
||||
|
|
@ -1130,12 +1132,11 @@ void Application::setupUICallbacks() {
|
|||
pos.z += 20.0f;
|
||||
}
|
||||
|
||||
// Nudge forward slightly to break edge-cases where unstuck lands exactly
|
||||
// on problematic collision seams.
|
||||
// Nudge forward to break free of collision seams / stuck geometry.
|
||||
if (gameHandler) {
|
||||
float renderYaw = gameHandler->getMovementInfo().orientation + glm::radians(90.0f);
|
||||
pos.x += std::cos(renderYaw) * 2.0f;
|
||||
pos.y += std::sin(renderYaw) * 2.0f;
|
||||
pos.x += std::cos(renderYaw) * 5.0f;
|
||||
pos.y += std::sin(renderYaw) * 5.0f;
|
||||
}
|
||||
|
||||
cc->teleportTo(pos);
|
||||
|
|
@ -4675,7 +4676,7 @@ void Application::processPendingMount() {
|
|||
}
|
||||
}
|
||||
|
||||
renderer->setMounted(instanceId, mountDisplayId, heightOffset);
|
||||
renderer->setMounted(instanceId, mountDisplayId, heightOffset, m2Path);
|
||||
charRenderer->playAnimation(instanceId, 0, true);
|
||||
|
||||
LOG_INFO("processPendingMount: DONE displayId=", mountDisplayId, " model=", m2Path, " heightOffset=", heightOffset);
|
||||
|
|
|
|||
|
|
@ -3280,6 +3280,18 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
mountAuraSpellId_ = a.spellId;
|
||||
}
|
||||
}
|
||||
// Classic/vanilla fallback: scan UNIT_FIELD_AURAS from same update block
|
||||
if (mountAuraSpellId_ == 0) {
|
||||
const uint16_t ufAuras = fieldIndex(UF::UNIT_FIELD_AURAS);
|
||||
if (ufAuras != 0xFFFF) {
|
||||
for (const auto& [fk, fv] : block.fields) {
|
||||
if (fk >= ufAuras && fk < ufAuras + 48 && fv != 0) {
|
||||
mountAuraSpellId_ = fv;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
LOG_INFO("Mount detected: displayId=", val, " auraSpellId=", mountAuraSpellId_);
|
||||
}
|
||||
if (old != 0 && val == 0) {
|
||||
|
|
@ -3670,6 +3682,18 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
mountAuraSpellId_ = a.spellId;
|
||||
}
|
||||
}
|
||||
// Classic/vanilla fallback: scan UNIT_FIELD_AURAS from same update block
|
||||
if (mountAuraSpellId_ == 0) {
|
||||
const uint16_t ufAuras = fieldIndex(UF::UNIT_FIELD_AURAS);
|
||||
if (ufAuras != 0xFFFF) {
|
||||
for (const auto& [fk, fv] : block.fields) {
|
||||
if (fk >= ufAuras && fk < ufAuras + 48 && fv != 0) {
|
||||
mountAuraSpellId_ = fv;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
LOG_INFO("Mount detected (values update): displayId=", val, " auraSpellId=", mountAuraSpellId_);
|
||||
}
|
||||
if (old != 0 && val == 0) {
|
||||
|
|
@ -8594,6 +8618,21 @@ void GameHandler::updateClientTaxi(float deltaTime) {
|
|||
auto playerEntity = entityManager.getEntity(playerGuid);
|
||||
|
||||
auto finishTaxiFlight = [&]() {
|
||||
// Snap player to the last waypoint (landing position) before clearing state.
|
||||
// Without this, the player would be left at whatever mid-flight position
|
||||
// they were at when the path completion was detected.
|
||||
if (!taxiClientPath_.empty()) {
|
||||
const auto& landingPos = taxiClientPath_.back();
|
||||
if (playerEntity) {
|
||||
playerEntity->setPosition(landingPos.x, landingPos.y, landingPos.z,
|
||||
movementInfo.orientation);
|
||||
}
|
||||
movementInfo.x = landingPos.x;
|
||||
movementInfo.y = landingPos.y;
|
||||
movementInfo.z = landingPos.z;
|
||||
LOG_INFO("Taxi landing: snapped to final waypoint (",
|
||||
landingPos.x, ", ", landingPos.y, ", ", landingPos.z, ")");
|
||||
}
|
||||
taxiClientActive_ = false;
|
||||
onTaxiFlight_ = false;
|
||||
taxiLandingCooldown_ = 2.0f; // 2 second cooldown to prevent re-entering
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ static const UFNameEntry kUFNames[] = {
|
|||
{"UNIT_FIELD_FLAGS_2", UF::UNIT_FIELD_FLAGS_2},
|
||||
{"UNIT_FIELD_DISPLAYID", UF::UNIT_FIELD_DISPLAYID},
|
||||
{"UNIT_FIELD_MOUNTDISPLAYID", UF::UNIT_FIELD_MOUNTDISPLAYID},
|
||||
{"UNIT_FIELD_AURAS", UF::UNIT_FIELD_AURAS},
|
||||
{"UNIT_NPC_FLAGS", UF::UNIT_NPC_FLAGS},
|
||||
{"UNIT_DYNAMIC_FLAGS", UF::UNIT_DYNAMIC_FLAGS},
|
||||
{"UNIT_END", UF::UNIT_END},
|
||||
|
|
|
|||
|
|
@ -895,7 +895,23 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) {
|
|||
(lowerName.find("seaweed") != std::string::npos) ||
|
||||
(lowerName.find("kelp") != std::string::npos) ||
|
||||
(lowerName.find("cattail") != std::string::npos) ||
|
||||
(lowerName.find("reed") != std::string::npos);
|
||||
(lowerName.find("reed") != std::string::npos) ||
|
||||
(lowerName.find("palm") != std::string::npos) ||
|
||||
(lowerName.find("bamboo") != std::string::npos) ||
|
||||
(lowerName.find("banana") != std::string::npos) ||
|
||||
(lowerName.find("coconut") != std::string::npos) ||
|
||||
(lowerName.find("canopy") != std::string::npos) ||
|
||||
(lowerName.find("hedge") != std::string::npos) ||
|
||||
(lowerName.find("cactus") != std::string::npos) ||
|
||||
(lowerName.find("leaf") != std::string::npos) ||
|
||||
(lowerName.find("leaves") != std::string::npos) ||
|
||||
(lowerName.find("stalk") != std::string::npos) ||
|
||||
(lowerName.find("corn") != std::string::npos) ||
|
||||
(lowerName.find("crop") != std::string::npos) ||
|
||||
(lowerName.find("hay") != std::string::npos) ||
|
||||
(lowerName.find("frond") != std::string::npos) ||
|
||||
(lowerName.find("algae") != std::string::npos) ||
|
||||
(lowerName.find("coral") != std::string::npos);
|
||||
bool treeLike = (lowerName.find("tree") != std::string::npos);
|
||||
foliageOrTreeLike = (foliageName || treeLike);
|
||||
bool hardTreePart =
|
||||
|
|
|
|||
|
|
@ -576,7 +576,7 @@ void Renderer::setCharacterFollow(uint32_t instanceId) {
|
|||
}
|
||||
}
|
||||
|
||||
void Renderer::setMounted(uint32_t mountInstId, uint32_t mountDisplayId, float heightOffset) {
|
||||
void Renderer::setMounted(uint32_t mountInstId, uint32_t mountDisplayId, float heightOffset, const std::string& modelPath) {
|
||||
mountInstanceId_ = mountInstId;
|
||||
mountHeightOffset_ = heightOffset;
|
||||
mountSeatAttachmentId_ = -1;
|
||||
|
|
@ -812,7 +812,7 @@ void Renderer::setMounted(uint32_t mountInstId, uint32_t mountDisplayId, float h
|
|||
// Notify mount sound manager
|
||||
if (mountSoundManager) {
|
||||
bool isFlying = taxiFlight_; // Taxi flights are flying mounts
|
||||
mountSoundManager->onMount(mountDisplayId, isFlying);
|
||||
mountSoundManager->onMount(mountDisplayId, isFlying, modelPath);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -400,9 +400,12 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) {
|
|||
}
|
||||
|
||||
groupRes.mergedBatches.reserve(batchMap.size());
|
||||
bool anyTextured = false;
|
||||
for (auto& [key, mb] : batchMap) {
|
||||
if (mb.hasTexture) anyTextured = true;
|
||||
groupRes.mergedBatches.push_back(std::move(mb));
|
||||
}
|
||||
groupRes.allUntextured = !anyTextured && !groupRes.mergedBatches.empty();
|
||||
}
|
||||
|
||||
// Copy portal data for visibility culling
|
||||
|
|
@ -1198,6 +1201,18 @@ void WMORenderer::render(const Camera& camera, const glm::mat4& view, const glm:
|
|||
for (uint32_t gi : dl.visibleGroups) {
|
||||
const auto& group = model.groups[gi];
|
||||
|
||||
// Skip groups with SHOW_SKYBOX flag (0x20000) — these are transparent
|
||||
// sky windows meant to show the skybox behind them, not solid geometry
|
||||
if (group.groupFlags & 0x20000) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip groups where ALL batches use the fallback white texture —
|
||||
// these are collision/placeholder/LOD shell groups with no visual data
|
||||
if (group.allUntextured) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// STORMWIND.WMO specific fix: LOD shell visibility control
|
||||
// Combination of distance culling + backface culling for best results
|
||||
bool isLODShell = false;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue