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:
Kelsi 2026-02-09 16:03:51 -08:00
parent 251f0ac246
commit 28d009f7db
8 changed files with 10908 additions and 198 deletions

View file

@ -3565,6 +3565,15 @@ void GameHandler::handleAttackStart(network::Packet& packet) {
} else if (data.victimGuid == playerGuid && data.attackerGuid != 0) {
hostileAttackers_.insert(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)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;
currentGossip = GossipMessageData{};
}
@ -4584,6 +4603,15 @@ void GameHandler::handleListInventory(network::Packet& packet) {
vendorWindowOpen = true;
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
for (const auto& item : currentVendorItems.items) {
queryItemInfo(item.itemId, 0);