You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
598 lines
16 KiB
598 lines
16 KiB
|
2 months ago
|
#include "utils/proximity_audio.hpp"
|
||
|
|
|
||
|
|
#include <algorithm>
|
||
|
|
#include <array>
|
||
|
|
#include <cctype>
|
||
|
|
#include <cmath>
|
||
|
|
#include <cstdint>
|
||
|
|
#include <memory>
|
||
|
|
#include <optional>
|
||
|
|
#include <string_view>
|
||
|
|
|
||
|
|
#ifdef USE_SDL3
|
||
|
|
#include <SDL3/SDL_timer.h>
|
||
|
|
#else
|
||
|
|
#include <SDL.h>
|
||
|
|
#endif
|
||
|
|
|
||
|
|
#include "controls/plrctrls.h"
|
||
|
|
#include "engine/assets.hpp"
|
||
|
|
#include "engine/path.h"
|
||
|
|
#include "engine/sound.h"
|
||
|
|
#include "engine/sound_position.hpp"
|
||
|
|
#include "inv.h"
|
||
|
|
#include "items.h"
|
||
|
|
#include "levels/gendung.h"
|
||
|
|
#include "levels/tile_properties.hpp"
|
||
|
|
#include "monster.h"
|
||
|
|
#include "objects.h"
|
||
|
|
#include "player.h"
|
||
|
|
#include "utils/is_of.hpp"
|
||
|
|
#include "utils/math.h"
|
||
|
|
#include "utils/screen_reader.hpp"
|
||
|
|
#include "utils/stdcompat/shared_ptr_array.hpp"
|
||
|
|
|
||
|
|
namespace devilution {
|
||
|
|
|
||
|
|
#ifdef NOSOUND
|
||
|
|
|
||
|
|
void UpdateProximityAudioCues()
|
||
|
|
{
|
||
|
|
}
|
||
|
|
|
||
|
|
#else
|
||
|
|
|
||
|
|
namespace {
|
||
|
|
|
||
|
|
constexpr int MaxCueDistanceTiles = 12;
|
||
|
|
constexpr int InteractDistanceTiles = 1;
|
||
|
|
|
||
|
|
// Pitch shifting via resampling caused audible glitches on some setups; keep cues at normal pitch for stability.
|
||
|
|
constexpr size_t PitchLevels = 1;
|
||
|
|
|
||
|
|
constexpr uint32_t MinIntervalMs = 250;
|
||
|
|
constexpr uint32_t MaxIntervalMs = 1000;
|
||
|
|
|
||
|
|
// Extra attenuation applied on top of CalculateSoundPosition().
|
||
|
|
// Kept at 0 because stronger attenuation makes distant proximity cues too quiet and feel "glitchy"/missing.
|
||
|
|
constexpr int ExtraAttenuationMax = 0;
|
||
|
|
|
||
|
|
struct CueSound {
|
||
|
|
std::array<std::unique_ptr<TSnd>, PitchLevels> variants;
|
||
|
|
|
||
|
|
[[nodiscard]] bool IsLoaded() const
|
||
|
|
{
|
||
|
|
for (const auto &variant : variants) {
|
||
|
|
if (variant != nullptr && variant->DSB.IsLoaded())
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
[[nodiscard]] bool IsAnyPlaying() const
|
||
|
|
{
|
||
|
|
for (const auto &variant : variants) {
|
||
|
|
if (variant != nullptr && variant->DSB.IsLoaded() && variant->DSB.IsPlaying())
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
std::optional<CueSound> WeaponItemCue;
|
||
|
|
std::optional<CueSound> ArmorItemCue;
|
||
|
|
std::optional<CueSound> GoldItemCue;
|
||
|
|
std::optional<CueSound> ChestCue;
|
||
|
|
std::optional<CueSound> DoorCue;
|
||
|
|
std::optional<CueSound> MonsterCue;
|
||
|
|
std::optional<CueSound> InteractCue;
|
||
|
|
|
||
|
|
std::array<uint32_t, MAXOBJECTS> LastObjectCueTimeMs {};
|
||
|
|
uint32_t LastMonsterCueTimeMs = 0;
|
||
|
|
std::optional<uint32_t> LastInteractableId;
|
||
|
|
uint32_t LastWeaponItemCueTimeMs = 0;
|
||
|
|
uint32_t LastArmorItemCueTimeMs = 0;
|
||
|
|
uint32_t LastGoldItemCueTimeMs = 0;
|
||
|
|
|
||
|
|
enum class InteractTargetType : uint8_t {
|
||
|
|
Item,
|
||
|
|
Object,
|
||
|
|
};
|
||
|
|
|
||
|
|
struct InteractTarget {
|
||
|
|
InteractTargetType type;
|
||
|
|
int id;
|
||
|
|
Point position;
|
||
|
|
};
|
||
|
|
|
||
|
|
[[nodiscard]] bool EndsWithCaseInsensitive(std::string_view str, std::string_view suffix)
|
||
|
|
{
|
||
|
|
if (str.size() < suffix.size())
|
||
|
|
return false;
|
||
|
|
const std::string_view tail { str.data() + (str.size() - suffix.size()), suffix.size() };
|
||
|
|
return std::equal(tail.begin(), tail.end(), suffix.begin(), suffix.end(), [](char a, char b) {
|
||
|
|
return std::tolower(static_cast<unsigned char>(a)) == std::tolower(static_cast<unsigned char>(b));
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
[[nodiscard]] bool IsMp3Path(std::string_view path)
|
||
|
|
{
|
||
|
|
return EndsWithCaseInsensitive(path, ".mp3");
|
||
|
|
}
|
||
|
|
|
||
|
|
[[nodiscard]] float PlaybackRateForPitchLevel(size_t level)
|
||
|
|
{
|
||
|
|
(void)level;
|
||
|
|
return 1.0F;
|
||
|
|
}
|
||
|
|
|
||
|
|
[[nodiscard]] size_t PitchLevelForDistance(int distance, int maxDistance)
|
||
|
|
{
|
||
|
|
(void)distance;
|
||
|
|
(void)maxDistance;
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
[[nodiscard]] uint32_t IntervalMsForDistance(int distance, int maxDistance)
|
||
|
|
{
|
||
|
|
if (maxDistance <= 0)
|
||
|
|
return MinIntervalMs;
|
||
|
|
|
||
|
|
const float t = std::clamp(static_cast<float>(distance) / static_cast<float>(maxDistance), 0.0F, 1.0F);
|
||
|
|
const float closeness = 1.0F - t;
|
||
|
|
const float interval = static_cast<float>(MaxIntervalMs) - closeness * static_cast<float>(MaxIntervalMs - MinIntervalMs);
|
||
|
|
return static_cast<uint32_t>(std::lround(interval));
|
||
|
|
}
|
||
|
|
|
||
|
|
std::optional<CueSound> TryLoadCueSound(std::initializer_list<std::string_view> candidatePaths)
|
||
|
|
{
|
||
|
|
if (!gbSndInited)
|
||
|
|
return std::nullopt;
|
||
|
|
|
||
|
|
for (std::string_view path : candidatePaths) {
|
||
|
|
AssetRef ref = FindAsset(path);
|
||
|
|
if (!ref.ok())
|
||
|
|
continue;
|
||
|
|
|
||
|
|
const size_t size = ref.size();
|
||
|
|
if (size == 0)
|
||
|
|
continue;
|
||
|
|
|
||
|
|
AssetHandle handle = OpenAsset(std::move(ref), /*threadsafe=*/true);
|
||
|
|
if (!handle.ok())
|
||
|
|
continue;
|
||
|
|
|
||
|
|
auto fileData = MakeArraySharedPtr<std::uint8_t>(size);
|
||
|
|
if (!handle.read(fileData.get(), size))
|
||
|
|
continue;
|
||
|
|
|
||
|
|
CueSound cue {};
|
||
|
|
bool ok = true;
|
||
|
|
|
||
|
|
for (size_t i = 0; i < PitchLevels; ++i) {
|
||
|
|
auto snd = std::make_unique<TSnd>();
|
||
|
|
snd->start_tc = SDL_GetTicks() - 80 - 1;
|
||
|
|
#ifndef NOSOUND
|
||
|
|
const bool isMp3 = IsMp3Path(path);
|
||
|
|
if (snd->DSB.SetChunk(fileData, size, isMp3, PlaybackRateForPitchLevel(i)) != 0) {
|
||
|
|
ok = false;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
#endif
|
||
|
|
cue.variants[i] = std::move(snd);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (ok)
|
||
|
|
return cue;
|
||
|
|
}
|
||
|
|
|
||
|
|
return std::nullopt;
|
||
|
|
}
|
||
|
|
|
||
|
|
void EnsureCuesLoaded()
|
||
|
|
{
|
||
|
|
static bool loaded = false;
|
||
|
|
if (loaded)
|
||
|
|
return;
|
||
|
|
|
||
|
|
WeaponItemCue = TryLoadCueSound({ "audio\\weapon.ogg", "..\\audio\\weapon.ogg", "audio\\weapon.wav", "..\\audio\\weapon.wav", "audio\\weapon.mp3", "..\\audio\\weapon.mp3" });
|
||
|
|
ArmorItemCue = TryLoadCueSound({ "audio\\armor.ogg", "..\\audio\\armor.ogg", "audio\\armor.wav", "..\\audio\\armor.wav", "audio\\armor.mp3", "..\\audio\\armor.mp3" });
|
||
|
|
GoldItemCue = TryLoadCueSound({ "audio\\coin.ogg", "..\\audio\\coin.ogg", "audio\\coin.wav", "..\\audio\\coin.wav", "audio\\coin.mp3", "..\\audio\\coin.mp3" });
|
||
|
|
|
||
|
|
ChestCue = TryLoadCueSound({ "audio\\chest.ogg", "..\\audio\\chest.ogg", "audio\\chest.wav", "..\\audio\\chest.wav", "audio\\chest.mp3", "..\\audio\\chest.mp3" });
|
||
|
|
DoorCue = TryLoadCueSound({ "audio\\door.ogg", "..\\audio\\door.ogg", "audio\\door.wav", "..\\audio\\door.wav", "audio\\Door.wav", "..\\audio\\Door.wav", "audio\\door.mp3", "..\\audio\\door.mp3" });
|
||
|
|
|
||
|
|
MonsterCue = TryLoadCueSound({ "audio\\monster.ogg", "..\\audio\\monster.ogg", "audio\\monster.wav", "..\\audio\\monster.wav", "audio\\monster.mp3", "..\\audio\\monster.mp3" });
|
||
|
|
|
||
|
|
InteractCue = TryLoadCueSound({ "audio\\interactispossible.ogg", "..\\audio\\interactispossible.ogg", "audio\\interactispossible.wav", "..\\audio\\interactispossible.wav", "audio\\interactispossible.mp3", "..\\audio\\interactispossible.mp3" });
|
||
|
|
|
||
|
|
loaded = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
[[nodiscard]] bool IsAnyCuePlaying()
|
||
|
|
{
|
||
|
|
const auto isAnyPlaying = [](const std::optional<CueSound> &cue) {
|
||
|
|
return cue && cue->IsAnyPlaying();
|
||
|
|
};
|
||
|
|
return isAnyPlaying(WeaponItemCue) || isAnyPlaying(ArmorItemCue) || isAnyPlaying(GoldItemCue) || isAnyPlaying(ChestCue) || isAnyPlaying(DoorCue)
|
||
|
|
|| isAnyPlaying(MonsterCue) || isAnyPlaying(InteractCue);
|
||
|
|
}
|
||
|
|
|
||
|
|
[[nodiscard]] bool PlayCueAt(const CueSound &cue, Point position, int distance, int maxDistance)
|
||
|
|
{
|
||
|
|
if (!gbSndInited || !gbSoundOn)
|
||
|
|
return false;
|
||
|
|
|
||
|
|
// Proximity cues are meant to guide the player; overlapping the same cue can create audio glitches/noise.
|
||
|
|
if (cue.IsAnyPlaying())
|
||
|
|
return false;
|
||
|
|
|
||
|
|
int logVolume = 0;
|
||
|
|
int logPan = 0;
|
||
|
|
if (!CalculateSoundPosition(position, &logVolume, &logPan))
|
||
|
|
return false;
|
||
|
|
|
||
|
|
const int extraAttenuation = static_cast<int>(std::lround(math::Remap(0, maxDistance, 0, ExtraAttenuationMax, distance)));
|
||
|
|
logVolume = std::max(ATTENUATION_MIN, logVolume - extraAttenuation);
|
||
|
|
if (logVolume <= ATTENUATION_MIN)
|
||
|
|
return false;
|
||
|
|
|
||
|
|
const size_t pitchLevel = std::min(PitchLevels - 1, PitchLevelForDistance(distance, maxDistance));
|
||
|
|
TSnd *snd = cue.variants[pitchLevel].get();
|
||
|
|
if (snd == nullptr || !snd->DSB.IsLoaded())
|
||
|
|
return false;
|
||
|
|
|
||
|
|
snd_play_snd(snd, logVolume, logPan);
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
[[nodiscard]] bool UpdateItemCues(const Point playerPosition, uint32_t now)
|
||
|
|
{
|
||
|
|
struct Candidate {
|
||
|
|
item_class itemClass;
|
||
|
|
int distance;
|
||
|
|
Point position;
|
||
|
|
};
|
||
|
|
|
||
|
|
std::optional<Candidate> nearest;
|
||
|
|
|
||
|
|
for (uint8_t i = 0; i < ActiveItemCount; i++) {
|
||
|
|
const int itemId = ActiveItems[i];
|
||
|
|
const Item &item = Items[itemId];
|
||
|
|
|
||
|
|
switch (item._iClass) {
|
||
|
|
case ICLASS_WEAPON:
|
||
|
|
break;
|
||
|
|
case ICLASS_ARMOR:
|
||
|
|
break;
|
||
|
|
case ICLASS_GOLD:
|
||
|
|
break;
|
||
|
|
default:
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
const int distance = playerPosition.ApproxDistance(item.position);
|
||
|
|
if (distance > MaxCueDistanceTiles)
|
||
|
|
continue;
|
||
|
|
|
||
|
|
if (!nearest || distance < nearest->distance)
|
||
|
|
nearest = Candidate { item._iClass, distance, item.position };
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!nearest)
|
||
|
|
return false;
|
||
|
|
|
||
|
|
const CueSound *cue = nullptr;
|
||
|
|
uint32_t *lastTimeMs = nullptr;
|
||
|
|
switch (nearest->itemClass) {
|
||
|
|
case ICLASS_WEAPON:
|
||
|
|
if (WeaponItemCue && WeaponItemCue->IsLoaded())
|
||
|
|
cue = &*WeaponItemCue;
|
||
|
|
lastTimeMs = &LastWeaponItemCueTimeMs;
|
||
|
|
break;
|
||
|
|
case ICLASS_ARMOR:
|
||
|
|
if (ArmorItemCue && ArmorItemCue->IsLoaded())
|
||
|
|
cue = &*ArmorItemCue;
|
||
|
|
lastTimeMs = &LastArmorItemCueTimeMs;
|
||
|
|
break;
|
||
|
|
case ICLASS_GOLD:
|
||
|
|
if (GoldItemCue && GoldItemCue->IsLoaded())
|
||
|
|
cue = &*GoldItemCue;
|
||
|
|
lastTimeMs = &LastGoldItemCueTimeMs;
|
||
|
|
break;
|
||
|
|
default:
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (cue == nullptr || lastTimeMs == nullptr)
|
||
|
|
return false;
|
||
|
|
|
||
|
|
const int distance = nearest->distance;
|
||
|
|
const uint32_t intervalMs = IntervalMsForDistance(distance, MaxCueDistanceTiles);
|
||
|
|
if (now - *lastTimeMs < intervalMs)
|
||
|
|
return false;
|
||
|
|
|
||
|
|
if (PlayCueAt(*cue, nearest->position, distance, MaxCueDistanceTiles)) {
|
||
|
|
*lastTimeMs = now;
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
[[nodiscard]] int GetRotaryDistanceForInteractTarget(const Player &player, Point destination)
|
||
|
|
{
|
||
|
|
if (player.position.future == destination)
|
||
|
|
return -1;
|
||
|
|
|
||
|
|
const int d1 = static_cast<int>(player._pdir);
|
||
|
|
const int d2 = static_cast<int>(GetDirection(player.position.future, destination));
|
||
|
|
|
||
|
|
const int d = std::abs(d1 - d2);
|
||
|
|
if (d > 4)
|
||
|
|
return 4 - (d % 4);
|
||
|
|
|
||
|
|
return d;
|
||
|
|
}
|
||
|
|
|
||
|
|
[[nodiscard]] bool IsReachableWithinSteps(const Player &player, Point start, Point destination, size_t maxSteps)
|
||
|
|
{
|
||
|
|
if (maxSteps == 0)
|
||
|
|
return start == destination;
|
||
|
|
|
||
|
|
if (start == destination)
|
||
|
|
return true;
|
||
|
|
|
||
|
|
if (start.WalkingDistance(destination) > static_cast<int>(maxSteps))
|
||
|
|
return false;
|
||
|
|
|
||
|
|
std::array<int8_t, InteractDistanceTiles> path;
|
||
|
|
path.fill(WALK_NONE);
|
||
|
|
|
||
|
|
const int steps = FindPath(CanStep, [&player](Point position) { return PosOkPlayer(player, position); }, start, destination, path.data(), path.size());
|
||
|
|
return steps != 0 && steps <= static_cast<int>(maxSteps);
|
||
|
|
}
|
||
|
|
|
||
|
|
std::optional<InteractTarget> FindInteractTargetInRange(const Player &player, Point playerPosition)
|
||
|
|
{
|
||
|
|
int rotations = 5;
|
||
|
|
std::optional<InteractTarget> best;
|
||
|
|
|
||
|
|
for (int dx = -1; dx <= 1; ++dx) {
|
||
|
|
for (int dy = -1; dy <= 1; ++dy) {
|
||
|
|
const Point targetPosition { playerPosition.x + dx, playerPosition.y + dy };
|
||
|
|
if (!InDungeonBounds(targetPosition))
|
||
|
|
continue;
|
||
|
|
|
||
|
|
const int itemId = dItem[targetPosition.x][targetPosition.y] - 1;
|
||
|
|
if (itemId < 0)
|
||
|
|
continue;
|
||
|
|
|
||
|
|
const Item &item = Items[itemId];
|
||
|
|
if (item.isEmpty() || item.selectionRegion == SelectionRegion::None)
|
||
|
|
continue;
|
||
|
|
|
||
|
|
const int newRotations = GetRotaryDistanceForInteractTarget(player, targetPosition);
|
||
|
|
if (rotations < newRotations)
|
||
|
|
continue;
|
||
|
|
if (targetPosition != playerPosition && !IsReachableWithinSteps(player, playerPosition, targetPosition, InteractDistanceTiles))
|
||
|
|
continue;
|
||
|
|
|
||
|
|
rotations = newRotations;
|
||
|
|
best = InteractTarget { .type = InteractTargetType::Item, .id = itemId, .position = targetPosition };
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (best)
|
||
|
|
return best;
|
||
|
|
|
||
|
|
rotations = 5;
|
||
|
|
|
||
|
|
for (int dx = -1; dx <= 1; ++dx) {
|
||
|
|
for (int dy = -1; dy <= 1; ++dy) {
|
||
|
|
const Point targetPosition { playerPosition.x + dx, playerPosition.y + dy };
|
||
|
|
if (!InDungeonBounds(targetPosition))
|
||
|
|
continue;
|
||
|
|
|
||
|
|
Object *object = FindObjectAtPosition(targetPosition);
|
||
|
|
if (object == nullptr || !object->canInteractWith())
|
||
|
|
continue;
|
||
|
|
if (!object->isDoor() && !object->IsChest())
|
||
|
|
continue;
|
||
|
|
if (object->IsDisabled())
|
||
|
|
continue;
|
||
|
|
if (targetPosition == playerPosition && object->_oDoorFlag)
|
||
|
|
continue;
|
||
|
|
|
||
|
|
const int newRotations = GetRotaryDistanceForInteractTarget(player, targetPosition);
|
||
|
|
if (rotations < newRotations)
|
||
|
|
continue;
|
||
|
|
if (targetPosition != playerPosition && !IsReachableWithinSteps(player, playerPosition, targetPosition, InteractDistanceTiles))
|
||
|
|
continue;
|
||
|
|
|
||
|
|
const int objectId = static_cast<int>(object - Objects);
|
||
|
|
rotations = newRotations;
|
||
|
|
best = InteractTarget { .type = InteractTargetType::Object, .id = objectId, .position = targetPosition };
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return best;
|
||
|
|
}
|
||
|
|
|
||
|
|
[[nodiscard]] bool UpdateObjectCues(const Point playerPosition, uint32_t now)
|
||
|
|
{
|
||
|
|
struct Candidate {
|
||
|
|
int objectId;
|
||
|
|
int distance;
|
||
|
|
const CueSound *cue;
|
||
|
|
};
|
||
|
|
|
||
|
|
std::optional<Candidate> nearest;
|
||
|
|
|
||
|
|
for (int i = 0; i < ActiveObjectCount; i++) {
|
||
|
|
const int objectId = ActiveObjects[i];
|
||
|
|
const Object &object = Objects[objectId];
|
||
|
|
if (!object.canInteractWith())
|
||
|
|
continue;
|
||
|
|
if (!object.isDoor() && !object.IsChest())
|
||
|
|
continue;
|
||
|
|
|
||
|
|
const int distance = playerPosition.ApproxDistance(object.position);
|
||
|
|
if (distance > MaxCueDistanceTiles)
|
||
|
|
continue;
|
||
|
|
|
||
|
|
const CueSound *cue = nullptr;
|
||
|
|
if (object.IsChest()) {
|
||
|
|
if (ChestCue && ChestCue->IsLoaded())
|
||
|
|
cue = &*ChestCue;
|
||
|
|
} else if (object.isDoor()) {
|
||
|
|
if (DoorCue && DoorCue->IsLoaded())
|
||
|
|
cue = &*DoorCue;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (cue == nullptr)
|
||
|
|
continue;
|
||
|
|
|
||
|
|
if (!nearest || distance < nearest->distance)
|
||
|
|
nearest = Candidate { objectId, distance, cue };
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!nearest)
|
||
|
|
return false;
|
||
|
|
|
||
|
|
const int objectId = nearest->objectId;
|
||
|
|
const int distance = nearest->distance;
|
||
|
|
const uint32_t intervalMs = IntervalMsForDistance(distance, MaxCueDistanceTiles);
|
||
|
|
if (now - LastObjectCueTimeMs[objectId] < intervalMs)
|
||
|
|
return false;
|
||
|
|
|
||
|
|
if (PlayCueAt(*nearest->cue, Objects[objectId].position, distance, MaxCueDistanceTiles)) {
|
||
|
|
LastObjectCueTimeMs[objectId] = now;
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
[[nodiscard]] bool UpdateMonsterCue(const Point playerPosition, uint32_t now)
|
||
|
|
{
|
||
|
|
if (!MonsterCue || !MonsterCue->IsLoaded())
|
||
|
|
return false;
|
||
|
|
|
||
|
|
std::optional<std::pair<int, Point>> nearest;
|
||
|
|
|
||
|
|
for (size_t i = 0; i < ActiveMonsterCount; i++) {
|
||
|
|
const int monsterId = static_cast<int>(ActiveMonsters[i]);
|
||
|
|
const Monster &monster = Monsters[monsterId];
|
||
|
|
|
||
|
|
if (monster.isInvalid)
|
||
|
|
continue;
|
||
|
|
if ((monster.flags & MFLAG_HIDDEN) != 0)
|
||
|
|
continue;
|
||
|
|
if (monster.hitPoints <= 0)
|
||
|
|
continue;
|
||
|
|
|
||
|
|
const Point monsterPosition { monster.position.tile };
|
||
|
|
const int distance = playerPosition.ApproxDistance(monsterPosition);
|
||
|
|
if (distance > MaxCueDistanceTiles)
|
||
|
|
continue;
|
||
|
|
|
||
|
|
if (!nearest || distance < nearest->first) {
|
||
|
|
nearest = { distance, monsterPosition };
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!nearest)
|
||
|
|
return false;
|
||
|
|
|
||
|
|
const int distance = nearest->first;
|
||
|
|
const uint32_t intervalMs = IntervalMsForDistance(distance, MaxCueDistanceTiles);
|
||
|
|
if (now - LastMonsterCueTimeMs < intervalMs)
|
||
|
|
return false;
|
||
|
|
|
||
|
|
if (!PlayCueAt(*MonsterCue, nearest->second, distance, MaxCueDistanceTiles))
|
||
|
|
return false;
|
||
|
|
|
||
|
|
LastMonsterCueTimeMs = now;
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
[[nodiscard]] bool UpdateInteractCue(const Point playerPosition, uint32_t now)
|
||
|
|
{
|
||
|
|
if (!InteractCue || !InteractCue->IsLoaded())
|
||
|
|
return false;
|
||
|
|
|
||
|
|
if (MyPlayer == nullptr)
|
||
|
|
return false;
|
||
|
|
|
||
|
|
const Player &player = *MyPlayer;
|
||
|
|
const std::optional<InteractTarget> target = FindInteractTargetInRange(player, playerPosition);
|
||
|
|
if (!target) {
|
||
|
|
LastInteractableId = std::nullopt;
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
const uint32_t id = target->type == InteractTargetType::Item
|
||
|
|
? (1U << 16) | static_cast<uint32_t>(target->id)
|
||
|
|
: (2U << 16) | static_cast<uint32_t>(target->id);
|
||
|
|
if (LastInteractableId && *LastInteractableId == id)
|
||
|
|
return false;
|
||
|
|
|
||
|
|
LastInteractableId = id;
|
||
|
|
|
||
|
|
if (!invflag) {
|
||
|
|
if (target->type == InteractTargetType::Item) {
|
||
|
|
const Item &item = Items[target->id];
|
||
|
|
const StringOrView name = item.getName();
|
||
|
|
if (!name.empty())
|
||
|
|
SpeakText(name.str(), /*force=*/true);
|
||
|
|
} else {
|
||
|
|
const Object &object = Objects[target->id];
|
||
|
|
const StringOrView name = object.name();
|
||
|
|
if (!name.empty())
|
||
|
|
SpeakText(name.str(), /*force=*/true);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!PlayCueAt(*InteractCue, target->position, /*distance=*/0, /*maxDistance=*/1))
|
||
|
|
return true;
|
||
|
|
(void)now;
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
} // namespace
|
||
|
|
|
||
|
|
void UpdateProximityAudioCues()
|
||
|
|
{
|
||
|
|
if (!gbSndInited || !gbSoundOn)
|
||
|
|
return;
|
||
|
|
if (leveltype == DTYPE_TOWN)
|
||
|
|
return;
|
||
|
|
if (MyPlayer == nullptr || MyPlayerIsDead || MyPlayer->_pmode == PM_DEATH)
|
||
|
|
return;
|
||
|
|
if (InGameMenu())
|
||
|
|
return;
|
||
|
|
|
||
|
|
EnsureCuesLoaded();
|
||
|
|
|
||
|
|
const uint32_t now = SDL_GetTicks();
|
||
|
|
const Point playerPosition { MyPlayer->position.future };
|
||
|
|
|
||
|
|
// Don't start another cue while one is playing (helps avoid overlap-related stutter/noise).
|
||
|
|
if (IsAnyCuePlaying())
|
||
|
|
return;
|
||
|
|
|
||
|
|
// Keep cues readable and reduce overlap/glitches by playing at most one per tick (priority order).
|
||
|
|
if (UpdateInteractCue(playerPosition, now))
|
||
|
|
return;
|
||
|
|
if (UpdateMonsterCue(playerPosition, now))
|
||
|
|
return;
|
||
|
|
if (UpdateItemCues(playerPosition, now))
|
||
|
|
return;
|
||
|
|
(void)UpdateObjectCues(playerPosition, now);
|
||
|
|
}
|
||
|
|
|
||
|
|
#endif // NOSOUND
|
||
|
|
|
||
|
|
} // namespace devilution
|