mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-03 08:03:50 +00:00
Add comprehensive NPC voice system with interaction and combat sounds
Implements full NPC voice interaction system supporting 6 different sound categories for all playable races/genders. System loads ~450+ voice clips from MPQ archives. Voice Categories: - Greeting: Play on NPC right-click interaction - Farewell: Play when closing gossip/dialog windows - Vendor: Play when opening merchant/vendor windows - Pissed: Play after clicking NPC 5+ times (spam protection) - Aggro: Play when NPC enters combat with player - Flee: Play when NPC is fleeing (ready for low-health triggers) Features: - Race/gender detection from NPC display IDs via CreatureDisplayInfoExtra.dbc - Intelligent click tracking for pissed sounds - Combat sounds use player character vocal files for humanoid NPCs - Cooldown system prevents voice spam (2s default, combat sounds bypass) - Generic fallback voices for unsupported NPC types - 3D positional audio support Voice Support: - All playable races: Human, Dwarf, Gnome, Night Elf, Orc, Tauren, Troll, Undead - Male and female variants for each race - StandardNPC sounds for social interactions - Character vocal sounds for combat Technical Changes: - Refactored NpcVoiceManager to support multiple sound categories - Added callbacks: NpcFarewell, NpcVendor, NpcAggro - Extended voice loading to parse both StandardNPC and Character vocal paths - Integrated with GameHandler for gossip, vendor, and combat events - Added detailed voice detection logging for debugging Also includes: - Sound manifest files added to docs/ for reference - Blacksmith hammer pitch increased to 1.6x (was 1.4x) - Blacksmith volume reduced 30% to 0.25 (was 0.35)
This commit is contained in:
parent
d071ca08e7
commit
e7daa71063
8 changed files with 10908 additions and 198 deletions
5415
docs/commonsound_manifest.txt
Normal file
5415
docs/commonsound_manifest.txt
Normal file
File diff suppressed because it is too large
Load diff
5098
docs/sound manifest speech-enUS.MPQ .txt
Normal file
5098
docs/sound manifest speech-enUS.MPQ .txt
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -47,26 +47,50 @@ public:
|
||||||
bool initialize(pipeline::AssetManager* assets);
|
bool initialize(pipeline::AssetManager* assets);
|
||||||
void shutdown();
|
void shutdown();
|
||||||
|
|
||||||
// Play greeting sound for NPC at given position
|
// Play NPC interaction sounds
|
||||||
void playGreeting(uint64_t npcGuid, VoiceType voiceType, const glm::vec3& position);
|
void playGreeting(uint64_t npcGuid, VoiceType voiceType, const glm::vec3& position);
|
||||||
|
void playFarewell(uint64_t npcGuid, VoiceType voiceType, const glm::vec3& position);
|
||||||
|
void playVendor(uint64_t npcGuid, VoiceType voiceType, const glm::vec3& position);
|
||||||
|
void playPissed(uint64_t npcGuid, VoiceType voiceType, const glm::vec3& position);
|
||||||
|
|
||||||
|
// Play NPC combat sounds
|
||||||
|
void playAggro(uint64_t npcGuid, VoiceType voiceType, const glm::vec3& position);
|
||||||
|
void playFlee(uint64_t npcGuid, VoiceType voiceType, const glm::vec3& position);
|
||||||
|
|
||||||
void setVolumeScale(float scale) { volumeScale_ = scale; }
|
void setVolumeScale(float scale) { volumeScale_ = scale; }
|
||||||
float getVolumeScale() const { return volumeScale_; }
|
float getVolumeScale() const { return volumeScale_; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
enum class SoundCategory {
|
||||||
|
GREETING,
|
||||||
|
FAREWELL,
|
||||||
|
VENDOR,
|
||||||
|
PISSED,
|
||||||
|
AGGRO,
|
||||||
|
FLEE
|
||||||
|
};
|
||||||
|
|
||||||
void loadVoiceSounds();
|
void loadVoiceSounds();
|
||||||
bool loadSound(const std::string& path, VoiceSample& sample);
|
bool loadSound(const std::string& path, VoiceSample& sample);
|
||||||
VoiceType detectVoiceType(uint32_t creatureEntry) const;
|
VoiceType detectVoiceType(uint32_t creatureEntry) const;
|
||||||
|
void playSound(uint64_t npcGuid, VoiceType voiceType, SoundCategory category, const glm::vec3& position);
|
||||||
|
|
||||||
pipeline::AssetManager* assetManager_ = nullptr;
|
pipeline::AssetManager* assetManager_ = nullptr;
|
||||||
float volumeScale_ = 1.0f;
|
float volumeScale_ = 1.0f;
|
||||||
|
|
||||||
// Voice samples grouped by type
|
// Voice samples grouped by type and category
|
||||||
std::unordered_map<VoiceType, std::vector<VoiceSample>> voiceLibrary_;
|
std::unordered_map<VoiceType, std::vector<VoiceSample>> greetingLibrary_;
|
||||||
|
std::unordered_map<VoiceType, std::vector<VoiceSample>> farewellLibrary_;
|
||||||
|
std::unordered_map<VoiceType, std::vector<VoiceSample>> vendorLibrary_;
|
||||||
|
std::unordered_map<VoiceType, std::vector<VoiceSample>> pissedLibrary_;
|
||||||
|
std::unordered_map<VoiceType, std::vector<VoiceSample>> aggroLibrary_;
|
||||||
|
std::unordered_map<VoiceType, std::vector<VoiceSample>> fleeLibrary_;
|
||||||
|
|
||||||
// Cooldown tracking (prevent spam clicking same NPC)
|
// Cooldown tracking (prevent spam clicking same NPC)
|
||||||
std::unordered_map<uint64_t, std::chrono::steady_clock::time_point> lastPlayTime_;
|
std::unordered_map<uint64_t, std::chrono::steady_clock::time_point> lastPlayTime_;
|
||||||
|
std::unordered_map<uint64_t, int> clickCount_; // Track clicks for pissed sounds
|
||||||
static constexpr float GREETING_COOLDOWN = 2.0f; // seconds
|
static constexpr float GREETING_COOLDOWN = 2.0f; // seconds
|
||||||
|
static constexpr int PISSED_CLICK_THRESHOLD = 5; // clicks before pissed
|
||||||
|
|
||||||
std::mt19937 rng_;
|
std::mt19937 rng_;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -345,6 +345,9 @@ public:
|
||||||
using NpcDeathCallback = std::function<void(uint64_t guid)>;
|
using NpcDeathCallback = std::function<void(uint64_t guid)>;
|
||||||
void setNpcDeathCallback(NpcDeathCallback cb) { npcDeathCallback_ = std::move(cb); }
|
void setNpcDeathCallback(NpcDeathCallback cb) { npcDeathCallback_ = std::move(cb); }
|
||||||
|
|
||||||
|
using NpcAggroCallback = std::function<void(uint64_t guid, const glm::vec3& position)>;
|
||||||
|
void setNpcAggroCallback(NpcAggroCallback cb) { npcAggroCallback_ = std::move(cb); }
|
||||||
|
|
||||||
// NPC respawn callback (health 0 → >0, resets animation to idle)
|
// NPC respawn callback (health 0 → >0, resets animation to idle)
|
||||||
using NpcRespawnCallback = std::function<void(uint64_t guid)>;
|
using NpcRespawnCallback = std::function<void(uint64_t guid)>;
|
||||||
void setNpcRespawnCallback(NpcRespawnCallback cb) { npcRespawnCallback_ = std::move(cb); }
|
void setNpcRespawnCallback(NpcRespawnCallback cb) { npcRespawnCallback_ = std::move(cb); }
|
||||||
|
|
@ -361,6 +364,12 @@ public:
|
||||||
using NpcGreetingCallback = std::function<void(uint64_t guid, const glm::vec3& position)>;
|
using NpcGreetingCallback = std::function<void(uint64_t guid, const glm::vec3& position)>;
|
||||||
void setNpcGreetingCallback(NpcGreetingCallback cb) { npcGreetingCallback_ = std::move(cb); }
|
void setNpcGreetingCallback(NpcGreetingCallback cb) { npcGreetingCallback_ = std::move(cb); }
|
||||||
|
|
||||||
|
using NpcFarewellCallback = std::function<void(uint64_t guid, const glm::vec3& position)>;
|
||||||
|
void setNpcFarewellCallback(NpcFarewellCallback cb) { npcFarewellCallback_ = std::move(cb); }
|
||||||
|
|
||||||
|
using NpcVendorCallback = std::function<void(uint64_t guid, const glm::vec3& position)>;
|
||||||
|
void setNpcVendorCallback(NpcVendorCallback cb) { npcVendorCallback_ = std::move(cb); }
|
||||||
|
|
||||||
// XP tracking
|
// XP tracking
|
||||||
uint32_t getPlayerXp() const { return playerXp_; }
|
uint32_t getPlayerXp() const { return playerXp_; }
|
||||||
uint32_t getPlayerNextLevelXp() const { return playerNextLevelXp_; }
|
uint32_t getPlayerNextLevelXp() const { return playerNextLevelXp_; }
|
||||||
|
|
@ -1031,10 +1040,13 @@ private:
|
||||||
void extractSkillFields(const std::map<uint16_t, uint32_t>& fields);
|
void extractSkillFields(const std::map<uint16_t, uint32_t>& fields);
|
||||||
|
|
||||||
NpcDeathCallback npcDeathCallback_;
|
NpcDeathCallback npcDeathCallback_;
|
||||||
|
NpcAggroCallback npcAggroCallback_;
|
||||||
NpcRespawnCallback npcRespawnCallback_;
|
NpcRespawnCallback npcRespawnCallback_;
|
||||||
MeleeSwingCallback meleeSwingCallback_;
|
MeleeSwingCallback meleeSwingCallback_;
|
||||||
NpcSwingCallback npcSwingCallback_;
|
NpcSwingCallback npcSwingCallback_;
|
||||||
NpcGreetingCallback npcGreetingCallback_;
|
NpcGreetingCallback npcGreetingCallback_;
|
||||||
|
NpcFarewellCallback npcFarewellCallback_;
|
||||||
|
NpcVendorCallback npcVendorCallback_;
|
||||||
MountCallback mountCallback_;
|
MountCallback mountCallback_;
|
||||||
TaxiPrecacheCallback taxiPrecacheCallback_;
|
TaxiPrecacheCallback taxiPrecacheCallback_;
|
||||||
TaxiOrientationCallback taxiOrientationCallback_;
|
TaxiOrientationCallback taxiOrientationCallback_;
|
||||||
|
|
|
||||||
|
|
@ -303,7 +303,7 @@ void AmbientSoundManager::updateBlacksmithAmbience(float deltaTime) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
float volume = 0.35f * volumeScale_; // Reduced from 0.7
|
float volume = 0.25f * volumeScale_; // Reduced 30% from 0.35
|
||||||
float pitch = 1.6f; // Higher pitch for metallic clink
|
float pitch = 1.6f; // Higher pitch for metallic clink
|
||||||
AudioEngine::instance().playSound2D(blacksmithSounds_[index].data, volume, pitch);
|
AudioEngine::instance().playSound2D(blacksmithSounds_[index].data, volume, pitch);
|
||||||
LOG_INFO("Playing blacksmith ambience (hammer strike)");
|
LOG_INFO("Playing blacksmith ambience (hammer strike)");
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ bool NpcVoiceManager::initialize(pipeline::AssetManager* assets) {
|
||||||
// Files are .WAV not .OGG in WotLK 3.3.5a!
|
// Files are .WAV not .OGG in WotLK 3.3.5a!
|
||||||
LOG_INFO("=== Probing for NPC voice files (.wav format) ===");
|
LOG_INFO("=== Probing for NPC voice files (.wav format) ===");
|
||||||
std::vector<std::string> testPaths = {
|
std::vector<std::string> testPaths = {
|
||||||
"Sound\\Creature\\HumanMaleStandardNPC\\HumanMaleStandardNPCGreeting01.wav",
|
"Sound\\Creature\\HumanMaleStandardNPC\\HumanMaleStandardNPCGreetings01.wav",
|
||||||
"Sound\\Creature\\HumanFemaleStandardNPC\\HumanFemaleStandardNPCGreeting01.wav",
|
"Sound\\Creature\\HumanFemaleStandardNPC\\HumanFemaleStandardNPCGreeting01.wav",
|
||||||
"Sound\\Creature\\DwarfMaleStandardNPC\\DwarfMaleStandardNPCGreeting01.wav",
|
"Sound\\Creature\\DwarfMaleStandardNPC\\DwarfMaleStandardNPCGreeting01.wav",
|
||||||
"Sound\\Creature\\OrcMaleStandardNPC\\OrcMaleStandardNPCGreeting01.wav",
|
"Sound\\Creature\\OrcMaleStandardNPC\\OrcMaleStandardNPCGreeting01.wav",
|
||||||
|
|
@ -32,260 +32,333 @@ bool NpcVoiceManager::initialize(pipeline::AssetManager* assets) {
|
||||||
bool exists = assetManager_->fileExists(path);
|
bool exists = assetManager_->fileExists(path);
|
||||||
LOG_INFO(" ", path, ": ", (exists ? "EXISTS" : "NOT FOUND"));
|
LOG_INFO(" ", path, ": ", (exists ? "EXISTS" : "NOT FOUND"));
|
||||||
}
|
}
|
||||||
LOG_INFO("=== Probing for tavern music files ===");
|
|
||||||
std::vector<std::string> musicPaths = {
|
|
||||||
"Sound\\Music\\GlueScreenMusic\\tavern_01.mp3",
|
|
||||||
"Sound\\Music\\GlueScreenMusic\\BC_Alehouse.mp3",
|
|
||||||
"Sound\\Music\\ZoneMusic\\Tavern\\tavernAlliance01.mp3",
|
|
||||||
};
|
|
||||||
for (const auto& path : musicPaths) {
|
|
||||||
bool exists = assetManager_->fileExists(path);
|
|
||||||
LOG_INFO(" ", path, ": ", (exists ? "EXISTS" : "NOT FOUND"));
|
|
||||||
}
|
|
||||||
LOG_INFO("===================================");
|
LOG_INFO("===================================");
|
||||||
|
|
||||||
loadVoiceSounds();
|
loadVoiceSounds();
|
||||||
|
|
||||||
int totalSamples = 0;
|
int totalSamples = 0;
|
||||||
for (const auto& [type, samples] : voiceLibrary_) {
|
for (const auto& [type, samples] : greetingLibrary_) totalSamples += samples.size();
|
||||||
totalSamples += samples.size();
|
for (const auto& [type, samples] : farewellLibrary_) totalSamples += samples.size();
|
||||||
}
|
for (const auto& [type, samples] : vendorLibrary_) totalSamples += samples.size();
|
||||||
|
for (const auto& [type, samples] : pissedLibrary_) totalSamples += samples.size();
|
||||||
|
for (const auto& [type, samples] : aggroLibrary_) totalSamples += samples.size();
|
||||||
|
for (const auto& [type, samples] : fleeLibrary_) totalSamples += samples.size();
|
||||||
LOG_INFO("NPC voice manager initialized (", totalSamples, " voice clips)");
|
LOG_INFO("NPC voice manager initialized (", totalSamples, " voice clips)");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void NpcVoiceManager::shutdown() {
|
void NpcVoiceManager::shutdown() {
|
||||||
voiceLibrary_.clear();
|
greetingLibrary_.clear();
|
||||||
|
farewellLibrary_.clear();
|
||||||
|
vendorLibrary_.clear();
|
||||||
|
pissedLibrary_.clear();
|
||||||
|
aggroLibrary_.clear();
|
||||||
|
fleeLibrary_.clear();
|
||||||
lastPlayTime_.clear();
|
lastPlayTime_.clear();
|
||||||
|
clickCount_.clear();
|
||||||
assetManager_ = nullptr;
|
assetManager_ = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
void NpcVoiceManager::loadVoiceSounds() {
|
void NpcVoiceManager::loadVoiceSounds() {
|
||||||
if (!assetManager_) return;
|
if (!assetManager_) return;
|
||||||
|
|
||||||
// WotLK 3.3.5a uses .WAV files, not .OGG!
|
// Helper to load voice category for a race/gender
|
||||||
// Files use "Greeting" (singular) not "Greetings"
|
auto loadCategory = [this](
|
||||||
|
std::unordered_map<VoiceType, std::vector<VoiceSample>>& library,
|
||||||
|
VoiceType type,
|
||||||
|
const std::string& npcType,
|
||||||
|
const std::string& soundType,
|
||||||
|
int count) {
|
||||||
|
|
||||||
// Generic - mix of all races for variety
|
auto& samples = library[type];
|
||||||
auto& genericVoices = voiceLibrary_[VoiceType::GENERIC];
|
for (int i = 1; i <= count; ++i) {
|
||||||
|
std::string num = (i < 10) ? ("0" + std::to_string(i)) : std::to_string(i);
|
||||||
|
std::string path = "Sound\\Creature\\" + npcType + "\\" + npcType + soundType + num + ".wav";
|
||||||
|
VoiceSample sample;
|
||||||
|
if (loadSound(path, sample)) samples.push_back(std::move(sample));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generic fallback voices (male only)
|
||||||
|
auto& genericGreet = greetingLibrary_[VoiceType::GENERIC];
|
||||||
for (const auto& path : {
|
for (const auto& path : {
|
||||||
"Sound\\Creature\\HumanMaleStandardNPC\\HumanMaleStandardNPCGreeting01.wav",
|
|
||||||
"Sound\\Creature\\HumanFemaleStandardNPC\\HumanFemaleStandardNPCGreeting01.wav",
|
|
||||||
"Sound\\Creature\\DwarfMaleStandardNPC\\DwarfMaleStandardNPCGreeting01.wav",
|
"Sound\\Creature\\DwarfMaleStandardNPC\\DwarfMaleStandardNPCGreeting01.wav",
|
||||||
"Sound\\Creature\\GnomeMaleStandardNPC\\GnomeMaleStandardNPCGreeting01.wav",
|
"Sound\\Creature\\GnomeMaleStandardNPC\\GnomeMaleStandardNPCGreeting01.wav",
|
||||||
"Sound\\Creature\\NightElfMaleStandardNPC\\NightElfMaleStandardNPCGreeting01.wav",
|
"Sound\\Creature\\NightElfMaleStandardNPC\\NightElfMaleStandardNPCGreeting01.wav",
|
||||||
"Sound\\Creature\\OrcMaleStandardNPC\\OrcMaleStandardNPCGreeting01.wav",
|
"Sound\\Creature\\OrcMaleStandardNPC\\OrcMaleStandardNPCGreeting01.wav",
|
||||||
}) {
|
}) {
|
||||||
VoiceSample sample;
|
VoiceSample sample;
|
||||||
if (loadSound(path, sample)) genericVoices.push_back(std::move(sample));
|
if (loadSound(path, sample)) genericGreet.push_back(std::move(sample));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Human Male
|
// Load all race/gender combinations
|
||||||
auto& humanMale = voiceLibrary_[VoiceType::HUMAN_MALE];
|
// Human Male uses "Greetings" (plural), others use "Greeting" (singular)
|
||||||
for (int i = 1; i <= 6; ++i) {
|
loadCategory(greetingLibrary_, VoiceType::HUMAN_MALE, "HumanMaleStandardNPC", "Greetings", 6);
|
||||||
std::string path = "Sound\\Creature\\HumanMaleStandardNPC\\HumanMaleStandardNPCGreeting0" + std::to_string(i) + ".wav";
|
loadCategory(farewellLibrary_, VoiceType::HUMAN_MALE, "HumanMaleStandardNPC", "Farewell", 5);
|
||||||
VoiceSample sample;
|
loadCategory(vendorLibrary_, VoiceType::HUMAN_MALE, "HumanMaleStandardNPC", "Vendor", 2);
|
||||||
if (loadSound(path, sample)) humanMale.push_back(std::move(sample));
|
loadCategory(pissedLibrary_, VoiceType::HUMAN_MALE, "HumanMaleStandardNPC", "Pissed", 4);
|
||||||
}
|
|
||||||
|
|
||||||
// Human Female
|
loadCategory(greetingLibrary_, VoiceType::HUMAN_FEMALE, "HumanFemaleStandardNPC", "Greeting", 5);
|
||||||
auto& humanFemale = voiceLibrary_[VoiceType::HUMAN_FEMALE];
|
loadCategory(farewellLibrary_, VoiceType::HUMAN_FEMALE, "HumanFemaleStandardNPC", "Farewell", 5);
|
||||||
for (int i = 1; i <= 5; ++i) {
|
loadCategory(vendorLibrary_, VoiceType::HUMAN_FEMALE, "HumanFemaleStandardNPC", "Vendor", 2);
|
||||||
std::string path = "Sound\\Creature\\HumanFemaleStandardNPC\\HumanFemaleStandardNPCGreeting0" + std::to_string(i) + ".wav";
|
loadCategory(pissedLibrary_, VoiceType::HUMAN_FEMALE, "HumanFemaleStandardNPC", "Pissed", 4);
|
||||||
VoiceSample sample;
|
|
||||||
if (loadSound(path, sample)) humanFemale.push_back(std::move(sample));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dwarf Male
|
loadCategory(greetingLibrary_, VoiceType::DWARF_MALE, "DwarfMaleStandardNPC", "Greeting", 6);
|
||||||
auto& dwarfMale = voiceLibrary_[VoiceType::DWARF_MALE];
|
loadCategory(farewellLibrary_, VoiceType::DWARF_MALE, "DwarfMaleStandardNPC", "Farewell", 4);
|
||||||
for (int i = 1; i <= 6; ++i) {
|
loadCategory(vendorLibrary_, VoiceType::DWARF_MALE, "DwarfMaleStandardNPC", "Vendor", 2);
|
||||||
std::string path = "Sound\\Creature\\DwarfMaleStandardNPC\\DwarfMaleStandardNPCGreeting0" + std::to_string(i) + ".wav";
|
loadCategory(pissedLibrary_, VoiceType::DWARF_MALE, "DwarfMaleStandardNPC", "Pissed", 4);
|
||||||
VoiceSample sample;
|
|
||||||
if (loadSound(path, sample)) dwarfMale.push_back(std::move(sample));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gnome Male
|
loadCategory(greetingLibrary_, VoiceType::GNOME_MALE, "GnomeMaleStandardNPC", "Greeting", 6);
|
||||||
auto& gnomeMale = voiceLibrary_[VoiceType::GNOME_MALE];
|
loadCategory(farewellLibrary_, VoiceType::GNOME_MALE, "GnomeMaleStandardNPC", "Farewell", 5);
|
||||||
for (int i = 1; i <= 6; ++i) {
|
loadCategory(vendorLibrary_, VoiceType::GNOME_MALE, "GnomeMaleStandardNPC", "Vendor", 2);
|
||||||
std::string path = "Sound\\Creature\\GnomeMaleStandardNPC\\GnomeMaleStandardNPCGreeting0" + std::to_string(i) + ".wav";
|
loadCategory(pissedLibrary_, VoiceType::GNOME_MALE, "GnomeMaleStandardNPC", "Pissed", 4);
|
||||||
VoiceSample sample;
|
|
||||||
if (loadSound(path, sample)) gnomeMale.push_back(std::move(sample));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gnome Female
|
loadCategory(greetingLibrary_, VoiceType::GNOME_FEMALE, "GnomeFemaleStandardNPC", "Greeting", 6);
|
||||||
auto& gnomeFemale = voiceLibrary_[VoiceType::GNOME_FEMALE];
|
loadCategory(farewellLibrary_, VoiceType::GNOME_FEMALE, "GnomeFemaleStandardNPC", "Farewell", 5);
|
||||||
for (int i = 1; i <= 6; ++i) {
|
loadCategory(vendorLibrary_, VoiceType::GNOME_FEMALE, "GnomeFemaleStandardNPC", "Vendor", 2);
|
||||||
std::string path = "Sound\\Creature\\GnomeFemaleStandardNPC\\GnomeFemaleStandardNPCGreeting0" + std::to_string(i) + ".wav";
|
loadCategory(pissedLibrary_, VoiceType::GNOME_FEMALE, "GnomeFemaleStandardNPC", "Pissed", 4);
|
||||||
VoiceSample sample;
|
|
||||||
if (loadSound(path, sample)) gnomeFemale.push_back(std::move(sample));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Night Elf Male
|
loadCategory(greetingLibrary_, VoiceType::NIGHTELF_MALE, "NightElfMaleStandardNPC", "Greeting", 8);
|
||||||
auto& nelfMale = voiceLibrary_[VoiceType::NIGHTELF_MALE];
|
loadCategory(farewellLibrary_, VoiceType::NIGHTELF_MALE, "NightElfMaleStandardNPC", "Farewell", 7);
|
||||||
for (int i = 1; i <= 8; ++i) {
|
loadCategory(vendorLibrary_, VoiceType::NIGHTELF_MALE, "NightElfMaleStandardNPC", "Vendor", 2);
|
||||||
std::string path = "Sound\\Creature\\NightElfMaleStandardNPC\\NightElfMaleStandardNPCGreeting0" + std::to_string(i) + ".wav";
|
loadCategory(pissedLibrary_, VoiceType::NIGHTELF_MALE, "NightElfMaleStandardNPC", "Pissed", 6);
|
||||||
VoiceSample sample;
|
|
||||||
if (loadSound(path, sample)) nelfMale.push_back(std::move(sample));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Night Elf Female
|
loadCategory(greetingLibrary_, VoiceType::NIGHTELF_FEMALE, "NightElfFemaleStandardNPC", "Greeting", 6);
|
||||||
auto& nelfFemale = voiceLibrary_[VoiceType::NIGHTELF_FEMALE];
|
loadCategory(farewellLibrary_, VoiceType::NIGHTELF_FEMALE, "NightElfFemaleStandardNPC", "Farewell", 6);
|
||||||
for (int i = 1; i <= 6; ++i) {
|
loadCategory(vendorLibrary_, VoiceType::NIGHTELF_FEMALE, "NightElfFemaleStandardNPC", "Vendor", 2);
|
||||||
std::string path = "Sound\\Creature\\NightElfFemaleStandardNPC\\NightElfFemaleStandardNPCGreeting0" + std::to_string(i) + ".wav";
|
loadCategory(pissedLibrary_, VoiceType::NIGHTELF_FEMALE, "NightElfFemaleStandardNPC", "Pissed", 6);
|
||||||
VoiceSample sample;
|
|
||||||
if (loadSound(path, sample)) nelfFemale.push_back(std::move(sample));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Orc Male
|
loadCategory(greetingLibrary_, VoiceType::ORC_MALE, "OrcMaleStandardNPC", "Greeting", 5);
|
||||||
auto& orcMale = voiceLibrary_[VoiceType::ORC_MALE];
|
loadCategory(farewellLibrary_, VoiceType::ORC_MALE, "OrcMaleStandardNPC", "Farewell", 5);
|
||||||
for (int i = 1; i <= 5; ++i) {
|
loadCategory(vendorLibrary_, VoiceType::ORC_MALE, "OrcMaleStandardNPC", "Vendor", 2);
|
||||||
std::string path = "Sound\\Creature\\OrcMaleStandardNPC\\OrcMaleStandardNPCGreeting0" + std::to_string(i) + ".wav";
|
loadCategory(pissedLibrary_, VoiceType::ORC_MALE, "OrcMaleStandardNPC", "Pissed", 4);
|
||||||
VoiceSample sample;
|
|
||||||
if (loadSound(path, sample)) orcMale.push_back(std::move(sample));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Orc Female
|
loadCategory(greetingLibrary_, VoiceType::ORC_FEMALE, "OrcFemaleStandardNPC", "Greeting", 6);
|
||||||
auto& orcFemale = voiceLibrary_[VoiceType::ORC_FEMALE];
|
loadCategory(farewellLibrary_, VoiceType::ORC_FEMALE, "OrcFemaleStandardNPC", "Farewell", 6);
|
||||||
for (int i = 1; i <= 6; ++i) {
|
loadCategory(vendorLibrary_, VoiceType::ORC_FEMALE, "OrcFemaleStandardNPC", "Vendor", 2);
|
||||||
std::string path = "Sound\\Creature\\OrcFemaleStandardNPC\\OrcFemaleStandardNPCGreeting0" + std::to_string(i) + ".wav";
|
loadCategory(pissedLibrary_, VoiceType::ORC_FEMALE, "OrcFemaleStandardNPC", "Pissed", 6);
|
||||||
VoiceSample sample;
|
|
||||||
if (loadSound(path, sample)) orcFemale.push_back(std::move(sample));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tauren Male
|
loadCategory(greetingLibrary_, VoiceType::TAUREN_MALE, "TaurenMaleStandardNPC", "Greeting", 5);
|
||||||
auto& taurenMale = voiceLibrary_[VoiceType::TAUREN_MALE];
|
loadCategory(farewellLibrary_, VoiceType::TAUREN_MALE, "TaurenMaleStandardNPC", "Farewell", 5);
|
||||||
for (int i = 1; i <= 5; ++i) {
|
// Tauren Male has no Vendor/Pissed sounds in manifest
|
||||||
std::string path = "Sound\\Creature\\TaurenMaleStandardNPC\\TaurenMaleStandardNPCGreeting0" + std::to_string(i) + ".wav";
|
|
||||||
VoiceSample sample;
|
|
||||||
if (loadSound(path, sample)) taurenMale.push_back(std::move(sample));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tauren Female
|
loadCategory(greetingLibrary_, VoiceType::TAUREN_FEMALE, "TaurenFemaleStandardNPC", "Greeting", 5);
|
||||||
auto& taurenFemale = voiceLibrary_[VoiceType::TAUREN_FEMALE];
|
loadCategory(farewellLibrary_, VoiceType::TAUREN_FEMALE, "TaurenFemaleStandardNPC", "Farewell", 5);
|
||||||
for (int i = 1; i <= 5; ++i) {
|
// Tauren Female has no Vendor/Pissed sounds in manifest
|
||||||
std::string path = "Sound\\Creature\\TaurenFemaleStandardNPC\\TaurenFemaleStandardNPCGreeting0" + std::to_string(i) + ".wav";
|
|
||||||
VoiceSample sample;
|
|
||||||
if (loadSound(path, sample)) taurenFemale.push_back(std::move(sample));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Troll Male
|
loadCategory(greetingLibrary_, VoiceType::TROLL_MALE, "TrollMaleStandardNPC", "Greeting", 6);
|
||||||
auto& trollMale = voiceLibrary_[VoiceType::TROLL_MALE];
|
loadCategory(farewellLibrary_, VoiceType::TROLL_MALE, "TrollMaleStandardNPC", "Farewell", 6);
|
||||||
for (int i = 1; i <= 6; ++i) {
|
loadCategory(vendorLibrary_, VoiceType::TROLL_MALE, "TrollMaleStandardNPC", "Vendor", 2);
|
||||||
std::string path = "Sound\\Creature\\TrollMaleStandardNPC\\TrollMaleStandardNPCGreeting0" + std::to_string(i) + ".wav";
|
loadCategory(pissedLibrary_, VoiceType::TROLL_MALE, "TrollMaleStandardNPC", "Pissed", 6);
|
||||||
VoiceSample sample;
|
|
||||||
if (loadSound(path, sample)) trollMale.push_back(std::move(sample));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Troll Female
|
loadCategory(greetingLibrary_, VoiceType::TROLL_FEMALE, "TrollFemaleStandardNPC", "Greeting", 5);
|
||||||
auto& trollFemale = voiceLibrary_[VoiceType::TROLL_FEMALE];
|
loadCategory(farewellLibrary_, VoiceType::TROLL_FEMALE, "TrollFemaleStandardNPC", "Farewell", 6);
|
||||||
for (int i = 1; i <= 5; ++i) {
|
loadCategory(vendorLibrary_, VoiceType::TROLL_FEMALE, "TrollFemaleStandardNPC", "Vendor", 2);
|
||||||
std::string path = "Sound\\Creature\\TrollFemaleStandardNPC\\TrollFemaleStandardNPCGreeting0" + std::to_string(i) + ".wav";
|
loadCategory(pissedLibrary_, VoiceType::TROLL_FEMALE, "TrollFemaleStandardNPC", "Pissed", 5);
|
||||||
VoiceSample sample;
|
|
||||||
if (loadSound(path, sample)) trollFemale.push_back(std::move(sample));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Undead Male
|
loadCategory(greetingLibrary_, VoiceType::UNDEAD_MALE, "UndeadMaleStandardNPC", "Greeting", 6);
|
||||||
auto& undeadMale = voiceLibrary_[VoiceType::UNDEAD_MALE];
|
loadCategory(farewellLibrary_, VoiceType::UNDEAD_MALE, "UndeadMaleStandardNPC", "Farewell", 6);
|
||||||
for (int i = 1; i <= 6; ++i) {
|
loadCategory(vendorLibrary_, VoiceType::UNDEAD_MALE, "UndeadMaleStandardNPC", "Vendor", 2);
|
||||||
std::string path = "Sound\\Creature\\UndeadMaleStandardNPC\\UndeadMaleStandardNPCGreeting0" + std::to_string(i) + ".wav";
|
loadCategory(pissedLibrary_, VoiceType::UNDEAD_MALE, "UndeadMaleStandardNPC", "Pissed", 6);
|
||||||
VoiceSample sample;
|
|
||||||
if (loadSound(path, sample)) undeadMale.push_back(std::move(sample));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Undead Female
|
loadCategory(greetingLibrary_, VoiceType::UNDEAD_FEMALE, "UndeadFemaleStandardNPC", "Greeting", 6);
|
||||||
auto& undeadFemale = voiceLibrary_[VoiceType::UNDEAD_FEMALE];
|
loadCategory(farewellLibrary_, VoiceType::UNDEAD_FEMALE, "UndeadFemaleStandardNPC", "Farewell", 6);
|
||||||
for (int i = 1; i <= 6; ++i) {
|
loadCategory(vendorLibrary_, VoiceType::UNDEAD_FEMALE, "UndeadFemaleStandardNPC", "Vendor", 2);
|
||||||
std::string path = "Sound\\Creature\\UndeadFemaleStandardNPC\\UndeadFemaleStandardNPCGreeting0" + std::to_string(i) + ".wav";
|
loadCategory(pissedLibrary_, VoiceType::UNDEAD_FEMALE, "UndeadFemaleStandardNPC", "Pissed", 6);
|
||||||
VoiceSample sample;
|
|
||||||
if (loadSound(path, sample)) undeadFemale.push_back(std::move(sample));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log loaded voice types
|
// Load combat sounds from Character vocal files
|
||||||
int totalLoaded = 0;
|
// These use a different path structure: Sound\Character\{Race}\{Race}Vocal{Gender}\{Race}{Gender}{Sound}.wav
|
||||||
for (const auto& [type, samples] : voiceLibrary_) {
|
auto loadCombatCategory = [this](
|
||||||
if (!samples.empty()) {
|
std::unordered_map<VoiceType, std::vector<VoiceSample>>& library,
|
||||||
LOG_INFO("Loaded ", samples.size(), " voice samples for type ", static_cast<int>(type));
|
VoiceType type,
|
||||||
totalLoaded += samples.size();
|
const std::string& raceFolder,
|
||||||
|
const std::string& raceGender,
|
||||||
|
const std::string& soundType,
|
||||||
|
int count) {
|
||||||
|
|
||||||
|
auto& samples = library[type];
|
||||||
|
for (int i = 1; i <= count; ++i) {
|
||||||
|
std::string num = (i < 10) ? ("0" + std::to_string(i)) : std::to_string(i);
|
||||||
|
std::string path = "Sound\\Character\\" + raceFolder + "\\" + raceFolder + "Vocal" +
|
||||||
|
(raceGender.find("Male") != std::string::npos ? "Male" : "Female") +
|
||||||
|
"\\" + raceGender + soundType + num + ".wav";
|
||||||
|
VoiceSample sample;
|
||||||
|
if (loadSound(path, sample)) samples.push_back(std::move(sample));
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
if (totalLoaded == 0) {
|
// Human combat sounds
|
||||||
LOG_WARNING("NPC voice manager: no voice samples loaded (files may not exist in MPQ)");
|
loadCombatCategory(aggroLibrary_, VoiceType::HUMAN_MALE, "Human", "HumanMale", "AttackMyTarget", 2);
|
||||||
}
|
loadCombatCategory(fleeLibrary_, VoiceType::HUMAN_MALE, "Human", "HumanMale", "Flee", 2);
|
||||||
|
|
||||||
|
loadCombatCategory(aggroLibrary_, VoiceType::HUMAN_FEMALE, "Human", "HumanFemale", "AttackMyTarget", 2);
|
||||||
|
loadCombatCategory(fleeLibrary_, VoiceType::HUMAN_FEMALE, "Human", "HumanFemale", "Flee", 2);
|
||||||
|
|
||||||
|
// Dwarf combat sounds
|
||||||
|
loadCombatCategory(aggroLibrary_, VoiceType::DWARF_MALE, "Dwarf", "DwarfMale", "AttackMyTarget", 3);
|
||||||
|
loadCombatCategory(fleeLibrary_, VoiceType::DWARF_MALE, "Dwarf", "DwarfMale", "Flee", 2);
|
||||||
|
|
||||||
|
// Gnome combat sounds
|
||||||
|
loadCombatCategory(aggroLibrary_, VoiceType::GNOME_MALE, "Gnome", "GnomeMale", "AttackMyTarget", 2);
|
||||||
|
loadCombatCategory(fleeLibrary_, VoiceType::GNOME_MALE, "Gnome", "GnomeMale", "Flee", 2);
|
||||||
|
|
||||||
|
loadCombatCategory(aggroLibrary_, VoiceType::GNOME_FEMALE, "Gnome", "GnomeFemale", "AttackMyTarget", 2);
|
||||||
|
loadCombatCategory(fleeLibrary_, VoiceType::GNOME_FEMALE, "Gnome", "GnomeFemale", "Flee", 2);
|
||||||
|
|
||||||
|
// Night Elf combat sounds
|
||||||
|
loadCombatCategory(aggroLibrary_, VoiceType::NIGHTELF_MALE, "NightElf", "NightElfMale", "AttackMyTarget", 2);
|
||||||
|
loadCombatCategory(fleeLibrary_, VoiceType::NIGHTELF_MALE, "NightElf", "NightElfMale", "Flee", 2);
|
||||||
|
|
||||||
|
loadCombatCategory(aggroLibrary_, VoiceType::NIGHTELF_FEMALE, "NightElf", "NightElfFemale", "AttackMyTarget", 3);
|
||||||
|
loadCombatCategory(fleeLibrary_, VoiceType::NIGHTELF_FEMALE, "NightElf", "NightElfFemale", "Flee", 2);
|
||||||
|
|
||||||
|
// Orc combat sounds
|
||||||
|
loadCombatCategory(aggroLibrary_, VoiceType::ORC_MALE, "Orc", "OrcMale", "AttackMyTarget", 3);
|
||||||
|
loadCombatCategory(fleeLibrary_, VoiceType::ORC_MALE, "Orc", "OrcMale", "Flee", 2);
|
||||||
|
|
||||||
|
loadCombatCategory(aggroLibrary_, VoiceType::ORC_FEMALE, "Orc", "OrcFemale", "AttackMyTarget", 3);
|
||||||
|
loadCombatCategory(fleeLibrary_, VoiceType::ORC_FEMALE, "Orc", "OrcFemale", "Flee", 2);
|
||||||
|
|
||||||
|
// Undead combat sounds (Scourge folder)
|
||||||
|
loadCombatCategory(aggroLibrary_, VoiceType::UNDEAD_MALE, "Scourge", "UndeadMale", "AttackMyTarget", 2);
|
||||||
|
loadCombatCategory(fleeLibrary_, VoiceType::UNDEAD_MALE, "Scourge", "UndeadMale", "Flee", 2);
|
||||||
|
|
||||||
|
loadCombatCategory(aggroLibrary_, VoiceType::UNDEAD_FEMALE, "Scourge", "UndeadFemale", "AttackMyTarget", 2);
|
||||||
|
loadCombatCategory(fleeLibrary_, VoiceType::UNDEAD_FEMALE, "Scourge", "UndeadFemale", "Flee", 2);
|
||||||
|
|
||||||
|
// Tauren combat sounds
|
||||||
|
loadCombatCategory(aggroLibrary_, VoiceType::TAUREN_MALE, "Tauren", "TaurenMale", "AttackMyTarget", 3);
|
||||||
|
loadCombatCategory(fleeLibrary_, VoiceType::TAUREN_MALE, "Tauren", "TaurenMale", "Flee", 2);
|
||||||
|
|
||||||
|
loadCombatCategory(aggroLibrary_, VoiceType::TAUREN_FEMALE, "Tauren", "TaurenFemale", "AttackMyTarget", 3);
|
||||||
|
loadCombatCategory(fleeLibrary_, VoiceType::TAUREN_FEMALE, "Tauren", "TaurenFemale", "Flee", 2);
|
||||||
|
|
||||||
|
// Troll combat sounds
|
||||||
|
loadCombatCategory(aggroLibrary_, VoiceType::TROLL_MALE, "Troll", "TrollMale", "AttackMyTarget", 3);
|
||||||
|
loadCombatCategory(fleeLibrary_, VoiceType::TROLL_MALE, "Troll", "TrollMale", "Flee", 2);
|
||||||
|
|
||||||
|
loadCombatCategory(aggroLibrary_, VoiceType::TROLL_FEMALE, "Troll", "TrollFemale", "AttackMyTarget", 3);
|
||||||
|
loadCombatCategory(fleeLibrary_, VoiceType::TROLL_FEMALE, "Troll", "TrollFemale", "Flee", 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool NpcVoiceManager::loadSound(const std::string& path, VoiceSample& sample) {
|
bool NpcVoiceManager::loadSound(const std::string& path, VoiceSample& sample) {
|
||||||
if (!assetManager_ || !assetManager_->fileExists(path)) {
|
if (!assetManager_) return false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto data = assetManager_->readFile(path);
|
if (!assetManager_->fileExists(path)) {
|
||||||
if (data.empty()) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
sample.path = path;
|
sample.path = path;
|
||||||
sample.data = std::move(data);
|
sample.data = assetManager_->readFile(path);
|
||||||
|
|
||||||
|
if (sample.data.empty()) {
|
||||||
|
LOG_WARNING("NPC voice: Failed to load sound data from ", path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void NpcVoiceManager::playGreeting(uint64_t npcGuid, VoiceType voiceType, const glm::vec3& position) {
|
void NpcVoiceManager::playSound(uint64_t npcGuid, VoiceType voiceType, SoundCategory category, const glm::vec3& position) {
|
||||||
LOG_INFO("NPC voice: playGreeting called for GUID ", npcGuid);
|
|
||||||
|
|
||||||
if (!AudioEngine::instance().isInitialized()) {
|
if (!AudioEngine::instance().isInitialized()) {
|
||||||
LOG_WARNING("NPC voice: AudioEngine not initialized");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check cooldown
|
// Check cooldown (except for pissed and combat sounds which override cooldown)
|
||||||
|
auto now = std::chrono::steady_clock::now();
|
||||||
|
if (category != SoundCategory::PISSED && category != SoundCategory::AGGRO && category != SoundCategory::FLEE) {
|
||||||
|
auto it = lastPlayTime_.find(npcGuid);
|
||||||
|
if (it != lastPlayTime_.end()) {
|
||||||
|
float elapsed = std::chrono::duration<float>(now - it->second).count();
|
||||||
|
if (elapsed < GREETING_COOLDOWN) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select library based on category
|
||||||
|
std::unordered_map<VoiceType, std::vector<VoiceSample>>* library = nullptr;
|
||||||
|
switch (category) {
|
||||||
|
case SoundCategory::GREETING: library = &greetingLibrary_; break;
|
||||||
|
case SoundCategory::FAREWELL: library = &farewellLibrary_; break;
|
||||||
|
case SoundCategory::VENDOR: library = &vendorLibrary_; break;
|
||||||
|
case SoundCategory::PISSED: library = &pissedLibrary_; break;
|
||||||
|
case SoundCategory::AGGRO: library = &aggroLibrary_; break;
|
||||||
|
case SoundCategory::FLEE: library = &fleeLibrary_; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find voice samples for this type
|
||||||
|
auto libIt = library->find(voiceType);
|
||||||
|
if (libIt == library->end() || libIt->second.empty()) {
|
||||||
|
// Fallback to GENERIC
|
||||||
|
libIt = library->find(VoiceType::GENERIC);
|
||||||
|
if (libIt == library->end() || libIt->second.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto& samples = libIt->second;
|
||||||
|
std::uniform_int_distribution<size_t> dist(0, samples.size() - 1);
|
||||||
|
const auto& sample = samples[dist(rng_)];
|
||||||
|
|
||||||
|
// Play sound
|
||||||
|
std::uniform_real_distribution<float> pitchDist(0.98f, 1.02f);
|
||||||
|
bool success = AudioEngine::instance().playSound3D(
|
||||||
|
sample.data,
|
||||||
|
position,
|
||||||
|
0.6f * volumeScale_,
|
||||||
|
pitchDist(rng_),
|
||||||
|
60.0f
|
||||||
|
);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
lastPlayTime_[npcGuid] = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void NpcVoiceManager::playGreeting(uint64_t npcGuid, VoiceType voiceType, const glm::vec3& position) {
|
||||||
|
// Check if on cooldown - if so, increment pissed counter instead
|
||||||
auto now = std::chrono::steady_clock::now();
|
auto now = std::chrono::steady_clock::now();
|
||||||
auto it = lastPlayTime_.find(npcGuid);
|
auto it = lastPlayTime_.find(npcGuid);
|
||||||
if (it != lastPlayTime_.end()) {
|
if (it != lastPlayTime_.end()) {
|
||||||
float elapsed = std::chrono::duration<float>(now - it->second).count();
|
float elapsed = std::chrono::duration<float>(now - it->second).count();
|
||||||
if (elapsed < GREETING_COOLDOWN) {
|
if (elapsed < GREETING_COOLDOWN) {
|
||||||
LOG_INFO("NPC voice: on cooldown (", elapsed, "s elapsed)");
|
// On cooldown - increment click count and maybe play pissed sound
|
||||||
return; // Still on cooldown
|
playPissed(npcGuid, voiceType, position);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find voice library for this type
|
// Reset click count on successful greeting
|
||||||
auto libIt = voiceLibrary_.find(voiceType);
|
clickCount_[npcGuid] = 0;
|
||||||
if (libIt == voiceLibrary_.end() || libIt->second.empty()) {
|
playSound(npcGuid, voiceType, SoundCategory::GREETING, position);
|
||||||
LOG_INFO("NPC voice: No samples for type ", static_cast<int>(voiceType), ", falling back to GENERIC");
|
}
|
||||||
// Fall back to generic
|
|
||||||
libIt = voiceLibrary_.find(VoiceType::GENERIC);
|
void NpcVoiceManager::playFarewell(uint64_t npcGuid, VoiceType voiceType, const glm::vec3& position) {
|
||||||
if (libIt == voiceLibrary_.end() || libIt->second.empty()) {
|
playSound(npcGuid, voiceType, SoundCategory::FAREWELL, position);
|
||||||
LOG_WARNING("NPC voice: No voice samples available (library empty)");
|
}
|
||||||
return; // No voice samples available
|
|
||||||
}
|
void NpcVoiceManager::playVendor(uint64_t npcGuid, VoiceType voiceType, const glm::vec3& position) {
|
||||||
|
playSound(npcGuid, voiceType, SoundCategory::VENDOR, position);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NpcVoiceManager::playPissed(uint64_t npcGuid, VoiceType voiceType, const glm::vec3& position) {
|
||||||
|
// Increment click count
|
||||||
|
clickCount_[npcGuid]++;
|
||||||
|
|
||||||
|
// Only play pissed sound after threshold
|
||||||
|
if (clickCount_[npcGuid] >= PISSED_CLICK_THRESHOLD) {
|
||||||
|
playSound(npcGuid, voiceType, SoundCategory::PISSED, position);
|
||||||
|
clickCount_[npcGuid] = 0; // Reset after playing
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const auto& samples = libIt->second;
|
void NpcVoiceManager::playAggro(uint64_t npcGuid, VoiceType voiceType, const glm::vec3& position) {
|
||||||
|
playSound(npcGuid, voiceType, SoundCategory::AGGRO, position);
|
||||||
|
}
|
||||||
|
|
||||||
// Pick random voice line
|
void NpcVoiceManager::playFlee(uint64_t npcGuid, VoiceType voiceType, const glm::vec3& position) {
|
||||||
std::uniform_int_distribution<size_t> dist(0, samples.size() - 1);
|
playSound(npcGuid, voiceType, SoundCategory::FLEE, position);
|
||||||
const auto& sample = samples[dist(rng_)];
|
|
||||||
|
|
||||||
LOG_INFO("NPC voice: Playing sound from: ", sample.path);
|
|
||||||
|
|
||||||
// Play with 3D positioning
|
|
||||||
std::uniform_real_distribution<float> volumeDist(0.85f, 1.0f);
|
|
||||||
std::uniform_real_distribution<float> pitchDist(0.98f, 1.02f);
|
|
||||||
|
|
||||||
bool success = AudioEngine::instance().playSound3D(
|
|
||||||
sample.data,
|
|
||||||
position,
|
|
||||||
volumeDist(rng_) * volumeScale_,
|
|
||||||
pitchDist(rng_),
|
|
||||||
40.0f // Max distance for voice
|
|
||||||
);
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
LOG_INFO("NPC voice: Sound played successfully");
|
|
||||||
lastPlayTime_[npcGuid] = now;
|
|
||||||
} else {
|
|
||||||
LOG_WARNING("NPC voice: Failed to play sound");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
VoiceType NpcVoiceManager::detectVoiceType(uint32_t creatureEntry) const {
|
VoiceType NpcVoiceManager::detectVoiceType(uint32_t creatureEntry) const {
|
||||||
|
|
|
||||||
|
|
@ -834,6 +834,57 @@ void Application::setupUICallbacks() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// NPC farewell callback - play farewell voice line
|
||||||
|
gameHandler->setNpcFarewellCallback([this](uint64_t guid, const glm::vec3& position) {
|
||||||
|
if (renderer && renderer->getNpcVoiceManager()) {
|
||||||
|
glm::vec3 renderPos = core::coords::canonicalToRender(position);
|
||||||
|
|
||||||
|
audio::VoiceType voiceType = audio::VoiceType::GENERIC;
|
||||||
|
auto entity = gameHandler->getEntityManager().getEntity(guid);
|
||||||
|
if (entity && entity->getType() == game::ObjectType::UNIT) {
|
||||||
|
auto unit = std::static_pointer_cast<game::Unit>(entity);
|
||||||
|
uint32_t displayId = unit->getDisplayId();
|
||||||
|
voiceType = detectVoiceTypeFromDisplayId(displayId);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer->getNpcVoiceManager()->playFarewell(guid, voiceType, renderPos);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// NPC vendor callback - play vendor voice line
|
||||||
|
gameHandler->setNpcVendorCallback([this](uint64_t guid, const glm::vec3& position) {
|
||||||
|
if (renderer && renderer->getNpcVoiceManager()) {
|
||||||
|
glm::vec3 renderPos = core::coords::canonicalToRender(position);
|
||||||
|
|
||||||
|
audio::VoiceType voiceType = audio::VoiceType::GENERIC;
|
||||||
|
auto entity = gameHandler->getEntityManager().getEntity(guid);
|
||||||
|
if (entity && entity->getType() == game::ObjectType::UNIT) {
|
||||||
|
auto unit = std::static_pointer_cast<game::Unit>(entity);
|
||||||
|
uint32_t displayId = unit->getDisplayId();
|
||||||
|
voiceType = detectVoiceTypeFromDisplayId(displayId);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer->getNpcVoiceManager()->playVendor(guid, voiceType, renderPos);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// NPC aggro callback - play combat start voice line
|
||||||
|
gameHandler->setNpcAggroCallback([this](uint64_t guid, const glm::vec3& position) {
|
||||||
|
if (renderer && renderer->getNpcVoiceManager()) {
|
||||||
|
glm::vec3 renderPos = core::coords::canonicalToRender(position);
|
||||||
|
|
||||||
|
audio::VoiceType voiceType = audio::VoiceType::GENERIC;
|
||||||
|
auto entity = gameHandler->getEntityManager().getEntity(guid);
|
||||||
|
if (entity && entity->getType() == game::ObjectType::UNIT) {
|
||||||
|
auto unit = std::static_pointer_cast<game::Unit>(entity);
|
||||||
|
uint32_t displayId = unit->getDisplayId();
|
||||||
|
voiceType = detectVoiceTypeFromDisplayId(displayId);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer->getNpcVoiceManager()->playAggro(guid, voiceType, renderPos);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// "Create Character" button on character screen
|
// "Create Character" button on character screen
|
||||||
uiManager->getCharacterScreen().setOnCreateCharacter([this]() {
|
uiManager->getCharacterScreen().setOnCreateCharacter([this]() {
|
||||||
uiManager->getCharacterCreateScreen().reset();
|
uiManager->getCharacterCreateScreen().reset();
|
||||||
|
|
@ -1936,32 +1987,41 @@ audio::VoiceType Application::detectVoiceTypeFromDisplayId(uint32_t displayId) c
|
||||||
// Look up display data
|
// Look up display data
|
||||||
auto itDisplay = displayDataMap_.find(displayId);
|
auto itDisplay = displayDataMap_.find(displayId);
|
||||||
if (itDisplay == displayDataMap_.end() || itDisplay->second.extraDisplayId == 0) {
|
if (itDisplay == displayDataMap_.end() || itDisplay->second.extraDisplayId == 0) {
|
||||||
|
LOG_INFO("Voice detection: displayId ", displayId, " -> GENERIC (no display data)");
|
||||||
return audio::VoiceType::GENERIC; // Not a humanoid or no extra data
|
return audio::VoiceType::GENERIC; // Not a humanoid or no extra data
|
||||||
}
|
}
|
||||||
|
|
||||||
// Look up humanoid extra data (race/sex info)
|
// Look up humanoid extra data (race/sex info)
|
||||||
auto itExtra = humanoidExtraMap_.find(itDisplay->second.extraDisplayId);
|
auto itExtra = humanoidExtraMap_.find(itDisplay->second.extraDisplayId);
|
||||||
if (itExtra == humanoidExtraMap_.end()) {
|
if (itExtra == humanoidExtraMap_.end()) {
|
||||||
|
LOG_INFO("Voice detection: displayId ", displayId, " -> GENERIC (no humanoid extra data)");
|
||||||
return audio::VoiceType::GENERIC;
|
return audio::VoiceType::GENERIC;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t raceId = itExtra->second.raceId;
|
uint8_t raceId = itExtra->second.raceId;
|
||||||
uint8_t sexId = itExtra->second.sexId;
|
uint8_t sexId = itExtra->second.sexId;
|
||||||
|
|
||||||
|
const char* raceName = "Unknown";
|
||||||
|
const char* sexName = (sexId == 0) ? "Male" : "Female";
|
||||||
|
|
||||||
// Map (raceId, sexId) to VoiceType
|
// Map (raceId, sexId) to VoiceType
|
||||||
// Race IDs: 1=Human, 2=Orc, 3=Dwarf, 4=NightElf, 5=Undead, 6=Tauren, 7=Gnome, 8=Troll
|
// Race IDs: 1=Human, 2=Orc, 3=Dwarf, 4=NightElf, 5=Undead, 6=Tauren, 7=Gnome, 8=Troll
|
||||||
// Sex IDs: 0=Male, 1=Female
|
// Sex IDs: 0=Male, 1=Female
|
||||||
|
audio::VoiceType result;
|
||||||
switch (raceId) {
|
switch (raceId) {
|
||||||
case 1: return (sexId == 0) ? audio::VoiceType::HUMAN_MALE : audio::VoiceType::HUMAN_FEMALE;
|
case 1: raceName = "Human"; result = (sexId == 0) ? audio::VoiceType::HUMAN_MALE : audio::VoiceType::HUMAN_FEMALE; break;
|
||||||
case 2: return (sexId == 0) ? audio::VoiceType::ORC_MALE : audio::VoiceType::ORC_FEMALE;
|
case 2: raceName = "Orc"; result = (sexId == 0) ? audio::VoiceType::ORC_MALE : audio::VoiceType::ORC_FEMALE; break;
|
||||||
case 3: return (sexId == 0) ? audio::VoiceType::DWARF_MALE : audio::VoiceType::GENERIC; // No dwarf female voices loaded
|
case 3: raceName = "Dwarf"; result = (sexId == 0) ? audio::VoiceType::DWARF_MALE : audio::VoiceType::GENERIC; break;
|
||||||
case 4: return (sexId == 0) ? audio::VoiceType::NIGHTELF_MALE : audio::VoiceType::NIGHTELF_FEMALE;
|
case 4: raceName = "NightElf"; result = (sexId == 0) ? audio::VoiceType::NIGHTELF_MALE : audio::VoiceType::NIGHTELF_FEMALE; break;
|
||||||
case 5: return (sexId == 0) ? audio::VoiceType::UNDEAD_MALE : audio::VoiceType::UNDEAD_FEMALE;
|
case 5: raceName = "Undead"; result = (sexId == 0) ? audio::VoiceType::UNDEAD_MALE : audio::VoiceType::UNDEAD_FEMALE; break;
|
||||||
case 6: return (sexId == 0) ? audio::VoiceType::TAUREN_MALE : audio::VoiceType::TAUREN_FEMALE;
|
case 6: raceName = "Tauren"; result = (sexId == 0) ? audio::VoiceType::TAUREN_MALE : audio::VoiceType::TAUREN_FEMALE; break;
|
||||||
case 7: return (sexId == 0) ? audio::VoiceType::GNOME_MALE : audio::VoiceType::GNOME_FEMALE;
|
case 7: raceName = "Gnome"; result = (sexId == 0) ? audio::VoiceType::GNOME_MALE : audio::VoiceType::GNOME_FEMALE; break;
|
||||||
case 8: return (sexId == 0) ? audio::VoiceType::TROLL_MALE : audio::VoiceType::TROLL_FEMALE;
|
case 8: raceName = "Troll"; result = (sexId == 0) ? audio::VoiceType::TROLL_MALE : audio::VoiceType::TROLL_FEMALE; break;
|
||||||
default: return audio::VoiceType::GENERIC;
|
default: result = audio::VoiceType::GENERIC; break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LOG_INFO("Voice detection: displayId ", displayId, " -> ", raceName, " ", sexName, " (race=", (int)raceId, ", sex=", (int)sexId, ")");
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Application::buildGameObjectDisplayLookups() {
|
void Application::buildGameObjectDisplayLookups() {
|
||||||
|
|
|
||||||
|
|
@ -3565,6 +3565,15 @@ void GameHandler::handleAttackStart(network::Packet& packet) {
|
||||||
} else if (data.victimGuid == playerGuid && data.attackerGuid != 0) {
|
} else if (data.victimGuid == playerGuid && data.attackerGuid != 0) {
|
||||||
hostileAttackers_.insert(data.attackerGuid);
|
hostileAttackers_.insert(data.attackerGuid);
|
||||||
autoTargetAttacker(data.attackerGuid);
|
autoTargetAttacker(data.attackerGuid);
|
||||||
|
|
||||||
|
// Play aggro sound when NPC attacks player
|
||||||
|
if (npcAggroCallback_) {
|
||||||
|
auto entity = entityManager.getEntity(data.attackerGuid);
|
||||||
|
if (entity && entity->getType() == ObjectType::UNIT) {
|
||||||
|
glm::vec3 pos(entity->getX(), entity->getY(), entity->getZ());
|
||||||
|
npcAggroCallback_(data.attackerGuid, pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4575,6 +4584,16 @@ void GameHandler::handleGossipMessage(network::Packet& packet) {
|
||||||
|
|
||||||
void GameHandler::handleGossipComplete(network::Packet& packet) {
|
void GameHandler::handleGossipComplete(network::Packet& packet) {
|
||||||
(void)packet;
|
(void)packet;
|
||||||
|
|
||||||
|
// Play farewell sound before closing
|
||||||
|
if (npcFarewellCallback_ && currentGossip.npcGuid != 0) {
|
||||||
|
auto entity = entityManager.getEntity(currentGossip.npcGuid);
|
||||||
|
if (entity && entity->getType() == ObjectType::UNIT) {
|
||||||
|
glm::vec3 pos(entity->getX(), entity->getY(), entity->getZ());
|
||||||
|
npcFarewellCallback_(currentGossip.npcGuid, pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
gossipWindowOpen = false;
|
gossipWindowOpen = false;
|
||||||
currentGossip = GossipMessageData{};
|
currentGossip = GossipMessageData{};
|
||||||
}
|
}
|
||||||
|
|
@ -4584,6 +4603,15 @@ void GameHandler::handleListInventory(network::Packet& packet) {
|
||||||
vendorWindowOpen = true;
|
vendorWindowOpen = true;
|
||||||
gossipWindowOpen = false; // Close gossip if vendor opens
|
gossipWindowOpen = false; // Close gossip if vendor opens
|
||||||
|
|
||||||
|
// Play vendor sound
|
||||||
|
if (npcVendorCallback_ && currentVendorItems.vendorGuid != 0) {
|
||||||
|
auto entity = entityManager.getEntity(currentVendorItems.vendorGuid);
|
||||||
|
if (entity && entity->getType() == ObjectType::UNIT) {
|
||||||
|
glm::vec3 pos(entity->getX(), entity->getY(), entity->getZ());
|
||||||
|
npcVendorCallback_(currentVendorItems.vendorGuid, pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Query item info for all vendor items so we can show names
|
// Query item info for all vendor items so we can show names
|
||||||
for (const auto& item : currentVendorItems.items) {
|
for (const auto& item : currentVendorItems.items) {
|
||||||
queryItemInfo(item.itemId, 0);
|
queryItemInfo(item.itemId, 0);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue