From 114b5793ee7f8e339f4c50b4db0913944e2e19c2 Mon Sep 17 00:00:00 2001 From: mojsior Date: Mon, 26 Jan 2026 17:20:14 +0100 Subject: [PATCH] Add melee monster TTS and audio cues volume --- Source/diablo.cpp | 75 ++++++++++++ Source/engine/sound.cpp | 51 +++++--- Source/engine/sound.h | 9 +- Source/engine/sound_pool.cpp | 11 +- Source/engine/sound_stubs.cpp | 11 +- Source/gamemenu.cpp | 221 +++++++++++++++++++--------------- Source/options.cpp | 36 +++--- Source/options.h | 22 ++-- 8 files changed, 282 insertions(+), 154 deletions(-) diff --git a/Source/diablo.cpp b/Source/diablo.cpp index 5a54a03ea..7642edc79 100644 --- a/Source/diablo.cpp +++ b/Source/diablo.cpp @@ -1901,6 +1901,80 @@ void UpdateBossHealthAnnouncements() } } +void UpdateAttackableMonsterAnnouncements() +{ + static std::optional LastAttackableMonsterId; + + if (MyPlayer == nullptr) { + LastAttackableMonsterId = std::nullopt; + return; + } + if (leveltype == DTYPE_TOWN) { + LastAttackableMonsterId = std::nullopt; + return; + } + if (MyPlayerIsDead || MyPlayer->_pmode == PM_DEATH || MyPlayer->hasNoLife()) { + LastAttackableMonsterId = std::nullopt; + return; + } + if (InGameMenu() || invflag) { + LastAttackableMonsterId = std::nullopt; + return; + } + + const Player &player = *MyPlayer; + const Point playerPosition = player.position.tile; + + int bestRotations = 5; + std::optional bestId; + + for (size_t i = 0; i < ActiveMonsterCount; i++) { + const int monsterId = static_cast(ActiveMonsters[i]); + const Monster &monster = Monsters[monsterId]; + + if (monster.isInvalid) + continue; + if ((monster.flags & MFLAG_HIDDEN) != 0) + continue; + if (monster.hitPoints <= 0) + continue; + if (monster.isPlayerMinion()) + continue; + if (!monster.isPossibleToHit()) + continue; + + const Point monsterPosition = monster.position.tile; + if (playerPosition.WalkingDistance(monsterPosition) > 1) + continue; + + const int d1 = static_cast(player._pdir); + const int d2 = static_cast(GetDirection(playerPosition, monsterPosition)); + + int rotations = std::abs(d1 - d2); + if (rotations > 4) + rotations = 4 - (rotations % 4); + + if (!bestId || rotations < bestRotations || (rotations == bestRotations && monsterId < *bestId)) { + bestRotations = rotations; + bestId = monsterId; + } + } + + if (!bestId) { + LastAttackableMonsterId = std::nullopt; + return; + } + + if (LastAttackableMonsterId && *LastAttackableMonsterId == *bestId) + return; + + LastAttackableMonsterId = *bestId; + + const std::string_view name = Monsters[*bestId].name(); + if (!name.empty()) + SpeakText(name, /*force=*/true); +} + void GameLogic() { if (!ProcessInput()) { @@ -1929,6 +2003,7 @@ void GameLogic() ProcessVisionList(); UpdateBossHealthAnnouncements(); UpdateProximityAudioCues(); + UpdateAttackableMonsterAnnouncements(); } else { gGameLogicStep = GameLogicStep::ProcessTowners; ProcessTowners(); diff --git a/Source/engine/sound.cpp b/Source/engine/sound.cpp index 9d20d0340..5e6ad537d 100644 --- a/Source/engine/sound.cpp +++ b/Source/engine/sound.cpp @@ -244,11 +244,12 @@ TSnd::~TSnd() DSB.Release(); } -void snd_init() -{ - GetOptions().Audio.soundVolume.SetValue(CapVolume(*GetOptions().Audio.soundVolume)); - gbSoundOn = *GetOptions().Audio.soundVolume > VOLUME_MIN; - sgbSaveSoundOn = gbSoundOn; +void snd_init() +{ + GetOptions().Audio.soundVolume.SetValue(CapVolume(*GetOptions().Audio.soundVolume)); + GetOptions().Audio.audioCuesVolume.SetValue(CapVolume(*GetOptions().Audio.audioCuesVolume)); + gbSoundOn = *GetOptions().Audio.soundVolume > VOLUME_MIN; + sgbSaveSoundOn = gbSoundOn; GetOptions().Audio.musicVolume.SetValue(CapVolume(*GetOptions().Audio.musicVolume)); gbMusicOn = *GetOptions().Audio.musicVolume > VOLUME_MIN; @@ -381,21 +382,31 @@ int sound_get_or_set_music_volume(int volume) return *GetOptions().Audio.musicVolume; } -int sound_get_or_set_sound_volume(int volume) -{ - if (volume == 1) - return *GetOptions().Audio.soundVolume; - - GetOptions().Audio.soundVolume.SetValue(volume); - - return *GetOptions().Audio.soundVolume; -} - -void music_mute() -{ - if (music.IsLoaded()) - music.Mute(); -} +int sound_get_or_set_sound_volume(int volume) +{ + if (volume == 1) + return *GetOptions().Audio.soundVolume; + + GetOptions().Audio.soundVolume.SetValue(volume); + + return *GetOptions().Audio.soundVolume; +} + +int sound_get_or_set_audio_cues_volume(int volume) +{ + if (volume == 1) + return *GetOptions().Audio.audioCuesVolume; + + GetOptions().Audio.audioCuesVolume.SetValue(volume); + + return *GetOptions().Audio.audioCuesVolume; +} + +void music_mute() +{ + if (music.IsLoaded()) + music.Mute(); +} void music_unmute() { diff --git a/Source/engine/sound.h b/Source/engine/sound.h index 0737f78f0..1a3ea272c 100644 --- a/Source/engine/sound.h +++ b/Source/engine/sound.h @@ -73,10 +73,11 @@ _music_id GetLevelMusic(dungeon_type dungeonType); void music_stop(); void music_start(_music_id nTrack); void sound_disable_music(bool disable); -int sound_get_or_set_music_volume(int volume); -int sound_get_or_set_sound_volume(int volume); -void music_mute(); -void music_unmute(); +int sound_get_or_set_music_volume(int volume); +int sound_get_or_set_sound_volume(int volume); +int sound_get_or_set_audio_cues_volume(int volume); +void music_mute(); +void music_unmute(); /* data */ diff --git a/Source/engine/sound_pool.cpp b/Source/engine/sound_pool.cpp index 5ff4c20b7..0b0baf210 100644 --- a/Source/engine/sound_pool.cpp +++ b/Source/engine/sound_pool.cpp @@ -116,7 +116,16 @@ struct SoundPool::Impl { if (sample.IsPlaying()) sample.Stop(); - return sample.PlayWithVolumeAndPan(logVolume, sound_get_or_set_sound_volume(/*volume=*/1), logPan); + const int soundVolume = sound_get_or_set_sound_volume(/*volume=*/1); + const int cuesVolume = sound_get_or_set_audio_cues_volume(/*volume=*/1); + + const int range = VOLUME_MAX - VOLUME_MIN; + const int soundOffset = std::clamp(soundVolume - VOLUME_MIN, 0, range); + const int cuesOffset = std::clamp(cuesVolume - VOLUME_MIN, 0, range); + const int combinedOffset = (soundOffset * cuesOffset + range / 2) / range; + const int combinedVolume = VOLUME_MIN + combinedOffset; + + return sample.PlayWithVolumeAndPan(logVolume, combinedVolume, logPan); } }; diff --git a/Source/engine/sound_stubs.cpp b/Source/engine/sound_stubs.cpp index 45f062b13..037173e14 100644 --- a/Source/engine/sound_stubs.cpp +++ b/Source/engine/sound_stubs.cpp @@ -18,10 +18,11 @@ void snd_deinit() { } void music_stop() { } void music_start(_music_id nTrack) { } void sound_disable_music(bool disable) { } -int sound_get_or_set_music_volume(int volume) { return 0; } -int sound_get_or_set_sound_volume(int volume) { return 0; } -void music_mute() { } -void music_unmute() { } -_music_id GetLevelMusic(dungeon_type dungeonType) { return TMUSIC_TOWN; } +int sound_get_or_set_music_volume(int volume) { return 0; } +int sound_get_or_set_sound_volume(int volume) { return 0; } +int sound_get_or_set_audio_cues_volume(int volume) { return 0; } +void music_mute() { } +void music_unmute() { } +_music_id GetLevelMusic(dungeon_type dungeonType) { return TMUSIC_TOWN; } } // namespace devilution diff --git a/Source/gamemenu.cpp b/Source/gamemenu.cpp index f1f997c75..596198918 100644 --- a/Source/gamemenu.cpp +++ b/Source/gamemenu.cpp @@ -39,11 +39,12 @@ namespace { // Forward-declare menu handlers, used by the global menu structs below. void GamemenuPrevious(bool bActivate); void GamemenuNewGame(bool bActivate); -void GamemenuOptions(bool bActivate); -void GamemenuMusicVolume(bool bActivate); -void GamemenuSoundVolume(bool bActivate); -void GamemenuBrightness(bool bActivate); -void GamemenuSpeed(bool bActivate); +void GamemenuOptions(bool bActivate); +void GamemenuMusicVolume(bool bActivate); +void GamemenuSoundVolume(bool bActivate); +void GamemenuAudioCuesVolume(bool bActivate); +void GamemenuBrightness(bool bActivate); +void GamemenuSpeed(bool bActivate); /** Contains the game menu items of the single player menu. */ TMenuItem sgSingleMenu[] = { @@ -67,27 +68,33 @@ TMenuItem sgMultiMenu[] = { { GMENU_ENABLED, nullptr, nullptr }, // clang-format on }; -TMenuItem sgOptionsMenu[] = { - // clang-format off - // dwFlags, pszStr, fnMenu - { GMENU_ENABLED | GMENU_SLIDER, nullptr, &GamemenuMusicVolume }, - { GMENU_ENABLED | GMENU_SLIDER, nullptr, &GamemenuSoundVolume }, - { GMENU_ENABLED | GMENU_SLIDER, N_("Gamma"), &GamemenuBrightness }, - { GMENU_ENABLED | GMENU_SLIDER, N_("Speed"), &GamemenuSpeed }, - { GMENU_ENABLED , N_("Previous Menu"), &GamemenuPrevious }, - { GMENU_ENABLED , nullptr, nullptr }, - // clang-format on -}; -/** Specifies the menu names for music enabled and disabled. */ -const char *const MusicToggleNames[] = { - N_("Music"), - N_("Music Disabled"), +TMenuItem sgOptionsMenu[] = { + // clang-format off + // dwFlags, pszStr, fnMenu + { GMENU_ENABLED | GMENU_SLIDER, nullptr, &GamemenuMusicVolume }, + { GMENU_ENABLED | GMENU_SLIDER, nullptr, &GamemenuSoundVolume }, + { GMENU_ENABLED | GMENU_SLIDER, nullptr, &GamemenuAudioCuesVolume }, + { GMENU_ENABLED | GMENU_SLIDER, N_("Gamma"), &GamemenuBrightness }, + { GMENU_ENABLED | GMENU_SLIDER, N_("Speed"), &GamemenuSpeed }, + { GMENU_ENABLED , N_("Previous Menu"), &GamemenuPrevious }, + { GMENU_ENABLED , nullptr, nullptr }, + // clang-format on +}; +/** Specifies the menu names for music enabled and disabled. */ +const char *const MusicToggleNames[] = { + N_("Music"), + N_("Music Disabled"), }; /** Specifies the menu names for sound enabled and disabled. */ -const char *const SoundToggleNames[] = { - N_("Sound"), - N_("Sound Disabled"), -}; +const char *const SoundToggleNames[] = { + N_("Sound"), + N_("Sound Disabled"), +}; +/** Specifies the menu names for navigation audio cues enabled and disabled. */ +const char *const AudioCuesToggleNames[] = { + N_("Audio Cues"), + N_("Audio Cues Disabled"), +}; void GamemenuUpdateSingle() { @@ -144,52 +151,58 @@ void GamemenuGetMusic() GamemenuSoundMusicToggle(MusicToggleNames, sgOptionsMenu, sound_get_or_set_music_volume(1)); } -void GamemenuGetSound() -{ - GamemenuSoundMusicToggle(SoundToggleNames, &sgOptionsMenu[1], sound_get_or_set_sound_volume(1)); -} - -void GamemenuGetBrightness() -{ - gmenu_slider_steps(&sgOptionsMenu[2], 21); - gmenu_slider_set(&sgOptionsMenu[2], 0, 100, UpdateBrightness(-1)); -} - -void GamemenuGetSpeed() -{ - if (gbIsMultiplayer) { - sgOptionsMenu[3].removeFlags(GMENU_ENABLED | GMENU_SLIDER); - if (sgGameInitInfo.nTickRate >= 50) - sgOptionsMenu[3].pszStr = _("Speed: Fastest").data(); - else if (sgGameInitInfo.nTickRate >= 40) - sgOptionsMenu[3].pszStr = _("Speed: Faster").data(); - else if (sgGameInitInfo.nTickRate >= 30) - sgOptionsMenu[3].pszStr = _("Speed: Fast").data(); - else if (sgGameInitInfo.nTickRate == 20) - sgOptionsMenu[3].pszStr = _("Speed: Normal").data(); - return; - } - - sgOptionsMenu[3].addFlags(GMENU_ENABLED | GMENU_SLIDER); - - sgOptionsMenu[3].pszStr = _("Speed").data(); - gmenu_slider_steps(&sgOptionsMenu[3], 46); - gmenu_slider_set(&sgOptionsMenu[3], 20, 50, sgGameInitInfo.nTickRate); -} - -int GamemenuSliderBrightness() -{ - return gmenu_slider_get(&sgOptionsMenu[2], 0, 100); -} - -void GamemenuOptions(bool /*bActivate*/) -{ - GamemenuGetMusic(); - GamemenuGetSound(); - GamemenuGetBrightness(); - GamemenuGetSpeed(); - gmenu_set_items(sgOptionsMenu, nullptr); -} +void GamemenuGetSound() +{ + GamemenuSoundMusicToggle(SoundToggleNames, &sgOptionsMenu[1], sound_get_or_set_sound_volume(1)); +} + +void GamemenuGetAudioCues() +{ + GamemenuSoundMusicToggle(AudioCuesToggleNames, &sgOptionsMenu[2], sound_get_or_set_audio_cues_volume(1)); +} + +void GamemenuGetBrightness() +{ + gmenu_slider_steps(&sgOptionsMenu[3], 21); + gmenu_slider_set(&sgOptionsMenu[3], 0, 100, UpdateBrightness(-1)); +} + +void GamemenuGetSpeed() +{ + if (gbIsMultiplayer) { + sgOptionsMenu[4].removeFlags(GMENU_ENABLED | GMENU_SLIDER); + if (sgGameInitInfo.nTickRate >= 50) + sgOptionsMenu[4].pszStr = _("Speed: Fastest").data(); + else if (sgGameInitInfo.nTickRate >= 40) + sgOptionsMenu[4].pszStr = _("Speed: Faster").data(); + else if (sgGameInitInfo.nTickRate >= 30) + sgOptionsMenu[4].pszStr = _("Speed: Fast").data(); + else if (sgGameInitInfo.nTickRate == 20) + sgOptionsMenu[4].pszStr = _("Speed: Normal").data(); + return; + } + + sgOptionsMenu[4].addFlags(GMENU_ENABLED | GMENU_SLIDER); + + sgOptionsMenu[4].pszStr = _("Speed").data(); + gmenu_slider_steps(&sgOptionsMenu[4], 46); + gmenu_slider_set(&sgOptionsMenu[4], 20, 50, sgGameInitInfo.nTickRate); +} + +int GamemenuSliderBrightness() +{ + return gmenu_slider_get(&sgOptionsMenu[3], 0, 100); +} + +void GamemenuOptions(bool /*bActivate*/) +{ + GamemenuGetMusic(); + GamemenuGetSound(); + GamemenuGetAudioCues(); + GamemenuGetBrightness(); + GamemenuGetSpeed(); + gmenu_set_items(sgOptionsMenu, nullptr); +} void GamemenuMusicVolume(bool bActivate) { @@ -220,10 +233,10 @@ void GamemenuMusicVolume(bool bActivate) GamemenuGetMusic(); } -void GamemenuSoundVolume(bool bActivate) -{ - if (bActivate) { - if (gbSoundOn) { +void GamemenuSoundVolume(bool bActivate) +{ + if (bActivate) { + if (gbSoundOn) { gbSoundOn = false; sound_stop(); sound_get_or_set_sound_volume(VOLUME_MIN); @@ -243,35 +256,49 @@ void GamemenuSoundVolume(bool bActivate) gbSoundOn = true; } } - PlaySFX(SfxID::MenuMove); - GamemenuGetSound(); -} - -void GamemenuBrightness(bool bActivate) -{ - int brightness; - if (bActivate) { + PlaySFX(SfxID::MenuMove); + GamemenuGetSound(); +} + +void GamemenuAudioCuesVolume(bool bActivate) +{ + if (bActivate) { + const int volume = sound_get_or_set_audio_cues_volume(1) == VOLUME_MIN ? VOLUME_MAX : VOLUME_MIN; + sound_get_or_set_audio_cues_volume(volume); + } else { + const int volume = GamemenuSliderMusicSound(&sgOptionsMenu[2]); + sound_get_or_set_audio_cues_volume(volume); + } + + PlaySFX(SfxID::MenuMove); + GamemenuGetAudioCues(); +} + +void GamemenuBrightness(bool bActivate) +{ + int brightness; + if (bActivate) { brightness = UpdateBrightness(-1); brightness = (brightness == 0) ? 100 : 0; } else { brightness = GamemenuSliderBrightness(); } - UpdateBrightness(brightness); - GamemenuGetBrightness(); -} - -void GamemenuSpeed(bool bActivate) -{ - if (bActivate) { - if (sgGameInitInfo.nTickRate != 20) - sgGameInitInfo.nTickRate = 20; - else - sgGameInitInfo.nTickRate = 50; - gmenu_slider_set(&sgOptionsMenu[3], 20, 50, sgGameInitInfo.nTickRate); - } else { - sgGameInitInfo.nTickRate = gmenu_slider_get(&sgOptionsMenu[3], 20, 50); - } + UpdateBrightness(brightness); + GamemenuGetBrightness(); +} + +void GamemenuSpeed(bool bActivate) +{ + if (bActivate) { + if (sgGameInitInfo.nTickRate != 20) + sgGameInitInfo.nTickRate = 20; + else + sgGameInitInfo.nTickRate = 50; + gmenu_slider_set(&sgOptionsMenu[4], 20, 50, sgGameInitInfo.nTickRate); + } else { + sgGameInitInfo.nTickRate = gmenu_slider_get(&sgOptionsMenu[4], 20, 50); + } GetOptions().Gameplay.tickRate.SetValue(sgGameInitInfo.nTickRate); gnTickDelay = 1000 / sgGameInitInfo.nTickRate; diff --git a/Source/options.cpp b/Source/options.cpp index ae5b2cabf..a055f81ba 100644 --- a/Source/options.cpp +++ b/Source/options.cpp @@ -508,28 +508,30 @@ std::vector HellfireOptions::GetEntries() }; } -AudioOptions::AudioOptions() - : OptionCategoryBase("Audio", N_("Audio"), N_("Audio Settings")) - , soundVolume("Sound Volume", OptionEntryFlags::Invisible, "Sound Volume", "Movie and SFX volume.", VOLUME_MAX) - , musicVolume("Music Volume", OptionEntryFlags::Invisible, "Music Volume", "Music Volume.", VOLUME_MAX) - , walkingSound("Walking Sound", OptionEntryFlags::None, N_("Walking Sound"), N_("Player emits sound when walking."), true) - , autoEquipSound("Auto Equip Sound", OptionEntryFlags::None, N_("Auto Equip Sound"), N_("Automatically equipping items on pickup emits the equipment sound."), false) - , itemPickupSound("Item Pickup Sound", OptionEntryFlags::None, N_("Item Pickup Sound"), N_("Picking up items emits the items pickup sound."), false) - , sampleRate("Sample Rate", OptionEntryFlags::CantChangeInGame, N_("Sample Rate"), N_("Output sample rate (Hz)."), DEFAULT_AUDIO_SAMPLE_RATE, { 22050, 44100, 48000 }) +AudioOptions::AudioOptions() + : OptionCategoryBase("Audio", N_("Audio"), N_("Audio Settings")) + , soundVolume("Sound Volume", OptionEntryFlags::Invisible, "Sound Volume", "Movie and SFX volume.", VOLUME_MAX) + , audioCuesVolume("Audio Cues Volume", OptionEntryFlags::Invisible, "Audio Cues Volume", "Navigation audio cues volume.", VOLUME_MAX) + , musicVolume("Music Volume", OptionEntryFlags::Invisible, "Music Volume", "Music Volume.", VOLUME_MAX) + , walkingSound("Walking Sound", OptionEntryFlags::None, N_("Walking Sound"), N_("Player emits sound when walking."), true) + , autoEquipSound("Auto Equip Sound", OptionEntryFlags::None, N_("Auto Equip Sound"), N_("Automatically equipping items on pickup emits the equipment sound."), false) + , itemPickupSound("Item Pickup Sound", OptionEntryFlags::None, N_("Item Pickup Sound"), N_("Picking up items emits the items pickup sound."), false) + , sampleRate("Sample Rate", OptionEntryFlags::CantChangeInGame, N_("Sample Rate"), N_("Output sample rate (Hz)."), DEFAULT_AUDIO_SAMPLE_RATE, { 22050, 44100, 48000 }) , channels("Channels", OptionEntryFlags::CantChangeInGame, N_("Channels"), N_("Number of output channels."), DEFAULT_AUDIO_CHANNELS, { 1, 2 }) , bufferSize("Buffer Size", OptionEntryFlags::CantChangeInGame, N_("Buffer Size"), N_("Buffer size (number of frames per channel)."), DEFAULT_AUDIO_BUFFER_SIZE, { 1024, 2048, 5120 }) , resamplingQuality("Resampling Quality", OptionEntryFlags::CantChangeInGame, N_("Resampling Quality"), N_("Quality of the resampler, from 0 (lowest) to 5 (highest)."), DEFAULT_AUDIO_RESAMPLING_QUALITY, { 0, 1, 2, 3, 4, 5 }) { } -std::vector AudioOptions::GetEntries() -{ - // clang-format off - return { - &soundVolume, - &musicVolume, - &walkingSound, - &autoEquipSound, - &itemPickupSound, +std::vector AudioOptions::GetEntries() +{ + // clang-format off + return { + &soundVolume, + &audioCuesVolume, + &musicVolume, + &walkingSound, + &autoEquipSound, + &itemPickupSound, &sampleRate, &channels, &bufferSize, diff --git a/Source/options.h b/Source/options.h index 7e4da902d..fa2ee0199 100644 --- a/Source/options.h +++ b/Source/options.h @@ -481,16 +481,18 @@ struct HellfireOptions : OptionCategoryBase { OptionEntryInt lastMultiplayerHero; }; -struct AudioOptions : OptionCategoryBase { - AudioOptions(); - std::vector GetEntries() override; - - /** @brief Movie and SFX volume. */ - OptionEntryInt soundVolume; - /** @brief Music volume. */ - OptionEntryInt musicVolume; - /** @brief Player emits sound when walking. */ - OptionEntryBoolean walkingSound; +struct AudioOptions : OptionCategoryBase { + AudioOptions(); + std::vector GetEntries() override; + + /** @brief Movie and SFX volume. */ + OptionEntryInt soundVolume; + /** @brief Navigation audio cues volume. */ + OptionEntryInt audioCuesVolume; + /** @brief Music volume. */ + OptionEntryInt musicVolume; + /** @brief Player emits sound when walking. */ + OptionEntryBoolean walkingSound; /** @brief Automatically equipping items on pickup emits the equipment sound. */ OptionEntryBoolean autoEquipSound; /** @brief Picking up items emits the items pickup sound. */