Browse Source

Add low HP warning sound and speak tracker target names

access
mojsior 2 months ago
parent
commit
a0ce2845a4
  1. 25
      README.md
  2. 164
      Source/diablo.cpp

25
README.md

@ -21,6 +21,31 @@ Upstream project: https://github.com/diasurgical/devilutionX
For a full list of changes, see our [changelog](docs/CHANGELOG.md).
# Features
- Screen reader / TTS integration (Windows: NVDA/JAWS via Tolk; Linux: speech-dispatcher).
- Proximity audio cues (items, monsters, doors/chests, interactable targets).
- Spoken announcements for many UI elements and interactions.
- Tracker navigation that speaks the tracked target and directions to reach it.
- Low HP warning sound that starts at 50% HP and speeds up every 10% down to 0% (stops on death).
# Keybinds (defaults)
Keybinds are configurable, but these are the defaults most players will use:
- `T` - cycle tracker target (items / chests / monsters).
- `N` - tracker directions to the nearest target (speaks target name + directions).
- `Shift`+`N` - retarget the tracker.
- `Ctrl`+`N` - clear the tracker target.
- `H` - speak nearest unexplored space.
- `E` - speak nearest exit.
- `,` - speak nearest stairs up.
- `.` - speak nearest stairs down.
- `F4` - list town NPCs.
- `PageUp` / `PageDown` - select previous / next town NPC.
- `End` - speak selected town NPC.
- `Home` - walk to selected town NPC (town only).
# How to Install
Note: You'll need access to the data from the original game. If you don't have an original CD, you can [buy Diablo from GoG.com](https://www.gog.com/game/diablo) or Battle.net. Alternatively, you can use `spawn.mpq` from the [shareware](https://github.com/diasurgical/devilutionx-assets/releases/latest/download/spawn.mpq) [[2]](http://ftp.blizzard.com/pub/demos/diablosw.exe) version, in place of `DIABDAT.MPQ`, to play the shareware portion of the game.

164
Source/diablo.cpp

@ -1604,11 +1604,11 @@ void UnstuckChargers()
}
}
void UpdateMonsterLights()
{
for (size_t i = 0; i < ActiveMonsterCount; i++) {
Monster &monster = Monsters[ActiveMonsters[i]];
void UpdateMonsterLights()
{
for (size_t i = 0; i < ActiveMonsterCount; i++) {
Monster &monster = Monsters[ActiveMonsters[i]];
if ((monster.flags & MFLAG_BERSERK) != 0) {
const int lightRadius = leveltype == DTYPE_NEST ? 9 : 3;
monster.lightId = AddLight(monster.position.tile, lightRadius);
@ -1625,14 +1625,129 @@ void UpdateMonsterLights()
ChangeLightXY(monster.lightId, monster.position.tile);
}
}
}
}
void GameLogic()
{
if (!ProcessInput()) {
return;
}
}
}
#ifdef NOSOUND
void UpdatePlayerLowHpWarningSound()
{
}
#else
namespace {
std::unique_ptr<TSnd> PlayerLowHpWarningSound;
bool TriedLoadingPlayerLowHpWarningSound = false;
TSnd *GetPlayerLowHpWarningSound()
{
if (TriedLoadingPlayerLowHpWarningSound)
return PlayerLowHpWarningSound.get();
TriedLoadingPlayerLowHpWarningSound = true;
if (!gbSndInited)
return nullptr;
PlayerLowHpWarningSound = std::make_unique<TSnd>();
PlayerLowHpWarningSound->start_tc = SDL_GetTicks() - 80 - 1;
// Support both the new "playerhaslowhp" name and the older underscore version.
if (PlayerLowHpWarningSound->DSB.SetChunkStream("audio\\playerhaslowhp.ogg", /*isMp3=*/false, /*logErrors=*/false) != 0
&& PlayerLowHpWarningSound->DSB.SetChunkStream("..\\audio\\playerhaslowhp.ogg", /*isMp3=*/false, /*logErrors=*/false) != 0
&& PlayerLowHpWarningSound->DSB.SetChunkStream("audio\\player_has_low_hp.ogg", /*isMp3=*/false, /*logErrors=*/false) != 0
&& PlayerLowHpWarningSound->DSB.SetChunkStream("..\\audio\\player_has_low_hp.ogg", /*isMp3=*/false, /*logErrors=*/false) != 0
&& PlayerLowHpWarningSound->DSB.SetChunkStream("audio\\playerhaslowhp.mp3", /*isMp3=*/true, /*logErrors=*/false) != 0
&& PlayerLowHpWarningSound->DSB.SetChunkStream("..\\audio\\playerhaslowhp.mp3", /*isMp3=*/true, /*logErrors=*/false) != 0
&& PlayerLowHpWarningSound->DSB.SetChunkStream("audio\\player_has_low_hp.mp3", /*isMp3=*/true, /*logErrors=*/false) != 0
&& PlayerLowHpWarningSound->DSB.SetChunkStream("..\\audio\\player_has_low_hp.mp3", /*isMp3=*/true, /*logErrors=*/false) != 0
&& PlayerLowHpWarningSound->DSB.SetChunkStream("audio\\playerhaslowhp.wav", /*isMp3=*/false, /*logErrors=*/false) != 0
&& PlayerLowHpWarningSound->DSB.SetChunkStream("..\\audio\\playerhaslowhp.wav", /*isMp3=*/false, /*logErrors=*/false) != 0
&& PlayerLowHpWarningSound->DSB.SetChunkStream("audio\\player_has_low_hp.wav", /*isMp3=*/false, /*logErrors=*/false) != 0
&& PlayerLowHpWarningSound->DSB.SetChunkStream("..\\audio\\player_has_low_hp.wav", /*isMp3=*/false, /*logErrors=*/false) != 0) {
PlayerLowHpWarningSound = nullptr;
}
return PlayerLowHpWarningSound.get();
}
void StopPlayerLowHpWarningSound()
{
if (PlayerLowHpWarningSound != nullptr)
PlayerLowHpWarningSound->DSB.Stop();
}
[[nodiscard]] uint32_t LowHpIntervalMs(int hpPercent)
{
// The sound starts at 50% HP (slow) and speeds up every 10% down to 0%.
if (hpPercent > 40)
return 1500;
if (hpPercent > 30)
return 1200;
if (hpPercent > 20)
return 900;
if (hpPercent > 10)
return 600;
return 300;
}
} // namespace
void UpdatePlayerLowHpWarningSound()
{
static uint32_t LastWarningStartMs = 0;
if (!gbSndInited || !gbSoundOn || MyPlayer == nullptr || InGameMenu()) {
StopPlayerLowHpWarningSound();
LastWarningStartMs = 0;
return;
}
// Stop immediately when dead.
if (MyPlayerIsDead || MyPlayer->_pmode == PM_DEATH || MyPlayer->hasNoLife()) {
StopPlayerLowHpWarningSound();
LastWarningStartMs = 0;
return;
}
const int maxHp = MyPlayer->_pMaxHP;
if (maxHp <= 0) {
StopPlayerLowHpWarningSound();
LastWarningStartMs = 0;
return;
}
const int hp = std::clamp(MyPlayer->_pHitPoints, 0, maxHp);
const int hpPercent = std::clamp(hp * 100 / maxHp, 0, 100);
// Only play below (or equal to) 50% and above 0%.
if (hpPercent > 50 || hpPercent <= 0) {
StopPlayerLowHpWarningSound();
LastWarningStartMs = 0;
return;
}
TSnd *snd = GetPlayerLowHpWarningSound();
if (snd == nullptr || !snd->DSB.IsLoaded())
return;
const uint32_t now = SDL_GetTicks();
const uint32_t intervalMs = LowHpIntervalMs(hpPercent);
if (LastWarningStartMs == 0)
LastWarningStartMs = now - intervalMs;
if (now - LastWarningStartMs < intervalMs)
return;
// Restart the cue even if it's already playing so the "tempo" is controlled by HP.
snd->DSB.Stop();
snd_play_snd(snd, /*lVolume=*/0, /*lPan=*/0);
LastWarningStartMs = now;
}
#endif // NOSOUND
void GameLogic()
{
if (!ProcessInput()) {
return;
}
if (gbProcessPlayers) {
gGameLogicStep = GameLogicStep::ProcessPlayers;
ProcessPlayers();
@ -1659,13 +1774,16 @@ void GameLogic()
ProcessTowners();
gGameLogicStep = GameLogicStep::ProcessItemsTown;
ProcessItems();
gGameLogicStep = GameLogicStep::ProcessMissilesTown;
ProcessMissiles();
}
gGameLogicStep = GameLogicStep::None;
#ifdef _DEBUG
if (DebugScrollViewEnabled && (SDL_GetModState() & SDL_KMOD_SHIFT) != 0) {
gGameLogicStep = GameLogicStep::ProcessMissilesTown;
ProcessMissiles();
}
UpdatePlayerLowHpWarningSound();
gGameLogicStep = GameLogicStep::None;
#ifdef _DEBUG
if (DebugScrollViewEnabled && (SDL_GetModState() & SDL_KMOD_SHIFT) != 0) {
ScrollView();
}
#endif
@ -2339,6 +2457,7 @@ void NavigateToTrackerTargetKeyPressed()
std::optional<int> targetId;
std::optional<Point> targetPosition;
StringOrView targetName;
switch (SelectedTrackerTargetCategory) {
case TrackerTargetCategory::Items: {
@ -2361,6 +2480,7 @@ void NavigateToTrackerTargetKeyPressed()
lockedTargetId = *targetId;
const Item &tracked = Items[*targetId];
targetName = tracked.getName();
targetPosition = tracked.position;
break;
}
@ -2388,6 +2508,7 @@ void NavigateToTrackerTargetKeyPressed()
lockedTargetId = *targetId;
const Object &tracked = Objects[*targetId];
targetName = tracked.name();
targetPosition = FindBestAdjacentApproachTile(*MyPlayer, playerPosition, tracked.position);
if (!targetPosition) {
SpeakText(_("Can't find a nearby tile to walk to."), true);
@ -2420,6 +2541,7 @@ void NavigateToTrackerTargetKeyPressed()
lockedTargetId = *targetId;
const Monster &tracked = Monsters[*targetId];
targetName = tracked.name();
const Point monsterPosition { tracked.position.tile };
targetPosition = FindBestAdjacentApproachTile(*MyPlayer, playerPosition, monsterPosition);
if (!targetPosition) {
@ -2436,6 +2558,8 @@ void NavigateToTrackerTargetKeyPressed()
const std::optional<std::vector<int8_t>> path = FindKeyboardWalkPathForSpeech(*MyPlayer, playerPosition, *targetPosition);
std::string message;
if (!targetName.empty())
StrAppend(message, targetName, "\n");
if (!path) {
AppendDirectionalFallback(message, *targetPosition - playerPosition);
} else {

Loading…
Cancel
Save