Browse Source

Add location/durability/boss HP announcements

- Mute proximity sound cues while inventory is open
- Speak current dungeon+floor on entry and via L
- Warn when equipped items are near breaking
- Announce boss HP every 10% drop
- Move Chat Log off L and migrate old bindings
access
mojsior 2 months ago
parent
commit
1142ac53f6
  1. 262
      Source/diablo.cpp
  2. 35
      Source/options.cpp
  3. 4
      Source/utils/proximity_audio.cpp
  4. 16
      Translations/pl.po

262
Source/diablo.cpp

@ -1746,6 +1746,156 @@ void UpdatePlayerLowHpWarningSound()
}
#endif // NOSOUND
namespace {
[[nodiscard]] bool IsBossMonsterForHpAnnouncement(const Monster &monster)
{
return monster.isUnique() || monster.ai == MonsterAIID::Diablo;
}
} // namespace
void UpdateLowDurabilityWarnings()
{
static std::array<uint32_t, NUM_INVLOC> WarnedSeeds {};
static std::array<bool, NUM_INVLOC> HasWarned {};
if (MyPlayer == nullptr)
return;
if (MyPlayerIsDead || MyPlayer->_pmode == PM_DEATH || MyPlayer->hasNoLife())
return;
std::vector<std::string> newlyLow;
newlyLow.reserve(NUM_INVLOC);
for (int slot = 0; slot < NUM_INVLOC; ++slot) {
const Item &item = MyPlayer->InvBody[slot];
if (item.isEmpty() || item._iMaxDur <= 0 || item._iMaxDur == DUR_INDESTRUCTIBLE || item._iDurability == DUR_INDESTRUCTIBLE) {
HasWarned[slot] = false;
continue;
}
const int maxDur = item._iMaxDur;
const int durability = item._iDurability;
if (durability <= 0) {
HasWarned[slot] = false;
continue;
}
int threshold = std::max(2, maxDur / 10);
threshold = std::clamp(threshold, 1, maxDur);
const bool isLow = durability <= threshold;
if (!isLow) {
HasWarned[slot] = false;
continue;
}
if (HasWarned[slot] && WarnedSeeds[slot] == item._iSeed)
continue;
HasWarned[slot] = true;
WarnedSeeds[slot] = item._iSeed;
const StringOrView name = item.getName();
if (!name.empty())
newlyLow.emplace_back(name.str().data(), name.str().size());
}
if (newlyLow.empty())
return;
// Add ordinal numbers for duplicates (e.g. two rings with the same name).
for (size_t i = 0; i < newlyLow.size(); ++i) {
int total = 0;
for (size_t j = 0; j < newlyLow.size(); ++j) {
if (newlyLow[j] == newlyLow[i])
++total;
}
if (total <= 1)
continue;
int ordinal = 1;
for (size_t j = 0; j < i; ++j) {
if (newlyLow[j] == newlyLow[i])
++ordinal;
}
newlyLow[i] = fmt::format("{} {}", newlyLow[i], ordinal);
}
std::string joined;
for (size_t i = 0; i < newlyLow.size(); ++i) {
if (i != 0)
joined += ", ";
joined += newlyLow[i];
}
SpeakText(fmt::format(fmt::runtime(_("Low durability: {:s}")), joined), /*force=*/true);
}
void UpdateBossHealthAnnouncements()
{
static dungeon_type LastLevelType = DTYPE_NONE;
static int LastCurrLevel = -1;
static bool LastSetLevel = false;
static _setlevels LastSetLevelNum = SL_NONE;
static std::array<int8_t, MaxMonsters> LastAnnouncedBucket {};
if (MyPlayer == nullptr)
return;
if (leveltype == DTYPE_TOWN)
return;
const bool levelChanged = LastLevelType != leveltype || LastCurrLevel != currlevel || LastSetLevel != setlevel || LastSetLevelNum != setlvlnum;
if (levelChanged) {
LastAnnouncedBucket.fill(-1);
LastLevelType = leveltype;
LastCurrLevel = currlevel;
LastSetLevel = setlevel;
LastSetLevelNum = setlvlnum;
}
for (size_t monsterId = 0; monsterId < MaxMonsters; ++monsterId) {
if (LastAnnouncedBucket[monsterId] < 0)
continue;
const Monster &monster = Monsters[monsterId];
if (monster.isInvalid || monster.hitPoints <= 0 || !IsBossMonsterForHpAnnouncement(monster))
LastAnnouncedBucket[monsterId] = -1;
}
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 (!IsBossMonsterForHpAnnouncement(monster))
continue;
if (monster.hitPoints <= 0 || monster.maxHitPoints <= 0)
continue;
const int64_t hp = std::clamp<int64_t>(monster.hitPoints, 0, monster.maxHitPoints);
const int64_t maxHp = monster.maxHitPoints;
const int hpPercent = static_cast<int>(std::clamp<int64_t>(hp * 100 / maxHp, 0, 100));
const int bucket = ((hpPercent + 9) / 10) * 10;
int8_t &lastBucket = LastAnnouncedBucket[monsterId];
if (lastBucket < 0) {
lastBucket = static_cast<int8_t>(((hpPercent + 9) / 10) * 10);
continue;
}
if (bucket >= lastBucket)
continue;
lastBucket = static_cast<int8_t>(bucket);
SpeakText(fmt::format(fmt::runtime(_("{:s} health: {:d}%")), monster.name(), bucket), /*force=*/false);
}
}
void GameLogic()
{
if (!ProcessInput()) {
@ -1756,11 +1906,12 @@ void GameLogic()
ProcessPlayers();
UpdateAutoWalkTownNpc();
UpdateAutoWalkTracker();
UpdateLowDurabilityWarnings();
}
if (leveltype != DTYPE_TOWN) {
gGameLogicStep = GameLogicStep::ProcessMonsters;
#ifdef _DEBUG
if (!DebugInvisible)
gGameLogicStep = GameLogicStep::ProcessMonsters;
#ifdef _DEBUG
if (!DebugInvisible)
#endif
ProcessMonsters();
gGameLogicStep = GameLogicStep::ProcessObjects;
@ -1771,6 +1922,7 @@ void GameLogic()
ProcessItems();
ProcessLightList();
ProcessVisionList();
UpdateBossHealthAnnouncements();
UpdateProximityAudioCues();
} else {
gGameLogicStep = GameLogicStep::ProcessTowners;
@ -4049,6 +4201,66 @@ void SpeakExperienceToNextLevelKeyPressed()
/*force=*/true);
}
namespace {
std::string BuildCurrentLocationForSpeech()
{
// Quest Level Name
if (setlevel) {
const char *const questLevelName = QuestLevelNames[setlvlnum];
if (questLevelName == nullptr || questLevelName[0] == '\0')
return std::string { _("Set level") };
return fmt::format("{:s}: {:s}", _("Set level"), _(questLevelName));
}
// Dungeon Name
constexpr std::array<const char *, DTYPE_LAST + 1> DungeonStrs = {
N_("Town"),
N_("Cathedral"),
N_("Catacombs"),
N_("Caves"),
N_("Hell"),
N_("Nest"),
N_("Crypt"),
};
std::string dungeonStr;
if (leveltype >= DTYPE_TOWN && leveltype <= DTYPE_LAST) {
dungeonStr = _(DungeonStrs[static_cast<size_t>(leveltype)]);
} else {
dungeonStr = _(/* TRANSLATORS: type of dungeon (i.e. Cathedral, Caves)*/ "None");
}
if (leveltype == DTYPE_TOWN || currlevel <= 0)
return dungeonStr;
// Dungeon Level
int level = currlevel;
if (leveltype == DTYPE_CATACOMBS)
level -= 4;
else if (leveltype == DTYPE_CAVES)
level -= 8;
else if (leveltype == DTYPE_HELL)
level -= 12;
else if (leveltype == DTYPE_NEST)
level -= 16;
else if (leveltype == DTYPE_CRYPT)
level -= 20;
if (level <= 0)
return dungeonStr;
return fmt::format(fmt::runtime(_(/* TRANSLATORS: dungeon type and floor number i.e. "Cathedral 3"*/ "{} {}")), dungeonStr, level);
}
} // namespace
void SpeakCurrentLocationKeyPressed()
{
if (!CanPlayerTakeAction())
return;
SpeakText(BuildCurrentLocationForSpeech(), /*force=*/true);
}
void InventoryKeyPressed()
{
if (IsPlayerInStore())
@ -4682,18 +4894,26 @@ void InitKeymapActions()
},
nullptr,
CanPlayerTakeAction);
options.Keymapper.AddAction(
"ChatLog",
N_("Chat Log"),
N_("Displays chat log."),
'L',
[] {
ToggleChatLog();
});
options.Keymapper.AddAction(
"SortInv",
N_("Sort Inventory"),
N_("Sorts the inventory."),
options.Keymapper.AddAction(
"ChatLog",
N_("Chat Log"),
N_("Displays chat log."),
SDLK_INSERT,
[] {
ToggleChatLog();
});
options.Keymapper.AddAction(
"SpeakCurrentLocation",
N_("Location"),
N_("Speaks the current dungeon and floor."),
'L',
SpeakCurrentLocationKeyPressed,
nullptr,
CanPlayerTakeAction);
options.Keymapper.AddAction(
"SortInv",
N_("Sort Inventory"),
N_("Sorts the inventory."),
'R',
[] {
ReorganizeInventory(*MyPlayer);
@ -6026,11 +6246,13 @@ tl::expected<void, std::string> LoadGameLevel(bool firstflag, lvl_entry lvldir)
#endif
LoadGameLevelStartMusic(neededTrack);
CompleteProgress();
LoadGameLevelCalculateCursor();
return {};
}
CompleteProgress();
LoadGameLevelCalculateCursor();
if (leveltype != DTYPE_TOWN)
SpeakText(BuildCurrentLocationForSpeech(), /*force=*/true);
return {};
}
bool game_loop(bool bStartup)
{

35
Source/options.cpp

@ -1194,6 +1194,14 @@ void KeymapperOptions::Action::LoadFromIni(std::string_view category)
const std::string_view iniValue = iniValues.back().value;
if (iniValue.empty()) {
if (key == "SpeakCurrentLocation") {
const std::span<const Ini::Value> chatLogValues = ini->get(category, "ChatLog");
if (!chatLogValues.empty() && chatLogValues.back().value == "L") {
SetValue(defaultKey);
return;
}
}
// Migration: some actions were previously saved as unbound because their default
// keys were not supported by the keymapper. If we see an explicit empty mapping
// for these actions, treat it as "use default".
@ -1207,17 +1215,22 @@ void KeymapperOptions::Action::LoadFromIni(std::string_view category)
}
auto keyIt = GetOptions().Keymapper.keyNameToKeyID.find(iniValue);
if (keyIt == GetOptions().Keymapper.keyNameToKeyID.end()) {
// Use the default key if the key is unknown.
Log("Keymapper: unknown key '{}'", iniValue);
SetValue(defaultKey);
return;
}
// Store the key in action.key and in the map so we can save() the
// actions while keeping the same order as they have been added.
SetValue(keyIt->second);
}
if (keyIt == GetOptions().Keymapper.keyNameToKeyID.end()) {
// Use the default key if the key is unknown.
Log("Keymapper: unknown key '{}'", iniValue);
SetValue(defaultKey);
return;
}
if (key == "ChatLog" && iniValue == "L") {
SetValue(defaultKey);
return;
}
// Store the key in action.key and in the map so we can save() the
// actions while keeping the same order as they have been added.
SetValue(keyIt->second);
}
void KeymapperOptions::Action::SaveToIni(std::string_view category) const
{
if (boundKey == SDLK_UNKNOWN) {

4
Source/utils/proximity_audio.cpp

@ -290,6 +290,10 @@ void UpdateProximityAudioCues()
return;
if (InGameMenu())
return;
if (invflag) {
SoundPool::Get().UpdateEmitters({}, SDL_GetTicks());
return;
}
SoundPool &pool = SoundPool::Get();
EnsureNavigationSoundsLoaded(pool);

16
Translations/pl.po

@ -6496,6 +6496,22 @@ msgstr "EXP do poziomu"
msgid "Speaks how much experience remains to reach the next level."
msgstr "Czyta, ile doświadczenia brakuje do następnego poziomu."
#: Source/diablo.cpp
msgid "Location"
msgstr "Lokalizacja"
#: Source/diablo.cpp
msgid "Speaks the current dungeon and floor."
msgstr "Czyta nazwę lochu i numer poziomu."
#: Source/diablo.cpp
msgid "Low durability: {:s}"
msgstr "Niska trwałość: {:s}"
#: Source/diablo.cpp
msgid "{:s} health: {:d}%"
msgstr "{:s} zdrowie: {:d}%"
#: Source/diablo.cpp
msgid "Max level."
msgstr "Maksymalny poziom."

Loading…
Cancel
Save