mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Add combat sounds, melee ability animations, and player vocalizations
Wire CombatSoundManager into SMSG_ATTACKERSTATEUPDATE for weapon swing, impact, and miss sounds. Add attack grunt and wound vocalizations to ActivitySoundManager using correct WoW MPQ PC-suffix paths. Trigger attack animation on SMSG_SPELL_GO for warrior melee abilities. Add client-side melee range and facing checks to prevent server rejections. Snap charge arrival to target's current position for reliable melee range.
This commit is contained in:
parent
e163813dee
commit
8a9d9f47db
4 changed files with 250 additions and 18 deletions
|
|
@ -408,10 +408,14 @@ void ActivitySoundManager::setCharacterVoiceProfile(const std::string& modelName
|
|||
rebuildJumpClipsForProfile(folder, base, male);
|
||||
rebuildSwimLoopClipsForProfile(folder, base, male);
|
||||
rebuildHardLandClipsForProfile(folder, base, male);
|
||||
rebuildCombatVocalClipsForProfile(folder, base, male);
|
||||
core::Logger::getInstance().info("Activity SFX voice profile: ", voiceProfileKey,
|
||||
" jump clips=", jumpClips.size(),
|
||||
" swim clips=", swimLoopClips.size(),
|
||||
" hardLand clips=", hardLandClips.size());
|
||||
" hardLand clips=", hardLandClips.size(),
|
||||
" attackGrunt clips=", attackGruntClips.size(),
|
||||
" wound clips=", woundClips.size(),
|
||||
" death clips=", deathClips.size());
|
||||
}
|
||||
|
||||
void ActivitySoundManager::playWaterEnter() {
|
||||
|
|
@ -448,5 +452,114 @@ void ActivitySoundManager::playWaterExit() {
|
|||
}
|
||||
}
|
||||
|
||||
void ActivitySoundManager::rebuildCombatVocalClipsForProfile(const std::string& raceFolder, const std::string& raceBase, bool male) {
|
||||
attackGruntClips.clear();
|
||||
woundClips.clear();
|
||||
woundCritClips.clear();
|
||||
deathClips.clear();
|
||||
|
||||
const std::string gender = male ? "Male" : "Female";
|
||||
// WoW MPQ convention: Sound\Character\{Race}{Gender}PC\{Race}{Gender}PC{Type}{Letter}.wav
|
||||
const std::string pcStem = raceBase + gender + "PC";
|
||||
const std::string pcPrefix = "Sound\\Character\\" + pcStem + "\\";
|
||||
// Fallback: Sound\Character\{Race}\{Race}{Gender}{Type}{Letter}.wav
|
||||
const std::string plainPrefix = "Sound\\Character\\" + raceFolder + "\\";
|
||||
const std::string plainStem = raceBase + gender;
|
||||
|
||||
// Attack grunts (A-I covers all races)
|
||||
std::vector<std::string> attackPaths;
|
||||
for (char c = 'A'; c <= 'I'; ++c) {
|
||||
std::string s(1, c);
|
||||
attackPaths.push_back(pcPrefix + pcStem + "Attack" + s + ".wav");
|
||||
}
|
||||
for (char c = 'A'; c <= 'I'; ++c) {
|
||||
std::string s(1, c);
|
||||
attackPaths.push_back(plainPrefix + plainStem + "Attack" + s + ".wav");
|
||||
}
|
||||
// Also try exertion sounds as attack grunts
|
||||
for (char c = 'A'; c <= 'F'; ++c) {
|
||||
std::string s(1, c);
|
||||
attackPaths.push_back(pcPrefix + pcStem + "Exertion" + s + ".wav");
|
||||
attackPaths.push_back(plainPrefix + plainStem + "Exertion" + s + ".wav");
|
||||
}
|
||||
preloadCandidates(attackGruntClips, attackPaths);
|
||||
|
||||
// Wound sounds (A-H covers all races)
|
||||
std::vector<std::string> woundPaths;
|
||||
for (char c = 'A'; c <= 'H'; ++c) {
|
||||
std::string s(1, c);
|
||||
woundPaths.push_back(pcPrefix + pcStem + "Wound" + s + ".wav");
|
||||
}
|
||||
for (char c = 'A'; c <= 'H'; ++c) {
|
||||
std::string s(1, c);
|
||||
woundPaths.push_back(plainPrefix + plainStem + "Wound" + s + ".wav");
|
||||
}
|
||||
preloadCandidates(woundClips, woundPaths);
|
||||
|
||||
// Wound crit sounds (A-C)
|
||||
std::vector<std::string> woundCritPaths;
|
||||
for (char c = 'A'; c <= 'C'; ++c) {
|
||||
std::string s(1, c);
|
||||
woundCritPaths.push_back(pcPrefix + pcStem + "WoundCrit" + s + ".wav");
|
||||
woundCritPaths.push_back(plainPrefix + plainStem + "WoundCrit" + s + ".wav");
|
||||
}
|
||||
preloadCandidates(woundCritClips, woundCritPaths);
|
||||
|
||||
// Death sounds
|
||||
preloadCandidates(deathClips, {
|
||||
pcPrefix + pcStem + "Death.wav",
|
||||
pcPrefix + pcStem + "Death2.wav",
|
||||
pcPrefix + pcStem + "DeathA.wav",
|
||||
pcPrefix + pcStem + "DeathB.wav",
|
||||
plainPrefix + plainStem + "Death.wav",
|
||||
plainPrefix + plainStem + "Death2.wav",
|
||||
plainPrefix + plainStem + "DeathA.wav",
|
||||
plainPrefix + plainStem + "DeathB.wav",
|
||||
});
|
||||
}
|
||||
|
||||
void ActivitySoundManager::playAttackGrunt() {
|
||||
if (!AudioEngine::instance().isInitialized() || attackGruntClips.empty()) return;
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
if (lastAttackGruntAt.time_since_epoch().count() != 0) {
|
||||
if (std::chrono::duration<float>(now - lastAttackGruntAt).count() < 1.5f) return;
|
||||
}
|
||||
// ~30% chance per swing to grunt (not every hit)
|
||||
std::uniform_int_distribution<int> chance(0, 9);
|
||||
if (chance(rng) > 2) return;
|
||||
|
||||
std::uniform_int_distribution<size_t> dist(0, attackGruntClips.size() - 1);
|
||||
const Sample& sample = attackGruntClips[dist(rng)];
|
||||
std::uniform_real_distribution<float> volDist(0.55f, 0.70f);
|
||||
std::uniform_real_distribution<float> pitchDist(0.96f, 1.04f);
|
||||
if (AudioEngine::instance().playSound2D(sample.data, volDist(rng) * volumeScale, pitchDist(rng))) {
|
||||
lastAttackGruntAt = now;
|
||||
}
|
||||
}
|
||||
|
||||
void ActivitySoundManager::playWound(bool isCrit) {
|
||||
if (!AudioEngine::instance().isInitialized()) return;
|
||||
auto& clips = (isCrit && !woundCritClips.empty()) ? woundCritClips : woundClips;
|
||||
if (clips.empty()) return;
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
if (lastWoundAt.time_since_epoch().count() != 0) {
|
||||
if (std::chrono::duration<float>(now - lastWoundAt).count() < 0.8f) return;
|
||||
}
|
||||
std::uniform_int_distribution<size_t> dist(0, clips.size() - 1);
|
||||
const Sample& sample = clips[dist(rng)];
|
||||
float vol = isCrit ? 0.80f : 0.65f;
|
||||
std::uniform_real_distribution<float> pitchDist(0.96f, 1.04f);
|
||||
if (AudioEngine::instance().playSound2D(sample.data, vol * volumeScale, pitchDist(rng))) {
|
||||
lastWoundAt = now;
|
||||
}
|
||||
}
|
||||
|
||||
void ActivitySoundManager::playDeath() {
|
||||
if (!AudioEngine::instance().isInitialized() || deathClips.empty()) return;
|
||||
std::uniform_int_distribution<size_t> dist(0, deathClips.size() - 1);
|
||||
const Sample& sample = deathClips[dist(rng)];
|
||||
AudioEngine::instance().playSound2D(sample.data, 0.85f * volumeScale, 1.0f);
|
||||
}
|
||||
|
||||
} // namespace audio
|
||||
} // namespace wowee
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue