Browse Source

Merge pull request #4 from ATGillespie25/refactor/seperate-accessibility-systems

pull/8474/head
mojsior 2 months ago committed by GitHub
parent
commit
facef187d8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 54
      Source/CMakeLists.txt
  2. 340
      Source/controls/accessibility_keys.cpp
  3. 30
      Source/controls/accessibility_keys.hpp
  4. 348
      Source/controls/town_npc_nav.cpp
  5. 19
      Source/controls/town_npc_nav.hpp
  6. 2859
      Source/controls/tracker.cpp
  7. 35
      Source/controls/tracker.hpp
  8. 11960
      Source/diablo.cpp
  9. 491
      Source/utils/accessibility_announcements.cpp
  10. 22
      Source/utils/accessibility_announcements.hpp
  11. 685
      Source/utils/navigation_speech.cpp
  12. 31
      Source/utils/navigation_speech.hpp
  13. 604
      Source/utils/walk_path_speech.cpp
  14. 44
      Source/utils/walk_path_speech.hpp

54
Source/CMakeLists.txt

@ -41,6 +41,7 @@ set(libdevilutionx_SRCS
control/control_infobox.cpp
control/control_panel.cpp
controls/accessibility_keys.cpp
controls/axis_direction.cpp
controls/controller_motion.cpp
controls/controller.cpp
@ -51,6 +52,8 @@ set(libdevilutionx_SRCS
controls/menu_controls.cpp
controls/modifier_hints.cpp
controls/plrctrls.cpp
controls/town_npc_nav.cpp
controls/tracker.cpp
DiabloUI/button.cpp
DiabloUI/credits.cpp
@ -160,13 +163,16 @@ set(libdevilutionx_SRCS
tables/textdat.cpp
tables/townerdat.cpp
utils/accessibility_announcements.cpp
utils/display.cpp
utils/language.cpp
utils/proximity_audio.cpp
utils/sdl_bilinear_scale.cpp
utils/sdl_thread.cpp
utils/surface_to_clx.cpp
utils/timer.cpp)
utils/language.cpp
utils/navigation_speech.cpp
utils/proximity_audio.cpp
utils/sdl_bilinear_scale.cpp
utils/sdl_thread.cpp
utils/surface_to_clx.cpp
utils/timer.cpp
utils/walk_path_speech.cpp)
# These files are responsible for most of the runtime in Debug mode.
# Apply some optimizations to them even in Debug mode to get reasonable performance.
@ -739,15 +745,15 @@ target_link_dependencies(libdevilutionx_utf8 PRIVATE
SheenBidi::SheenBidi
)
if(NOSOUND)
add_devilutionx_object_library(libdevilutionx_sound
effects_stubs.cpp
engine/sound_pool_stubs.cpp
engine/sound_stubs.cpp
)
target_link_dependencies(libdevilutionx_sound PUBLIC
DevilutionX::SDL
fmt::fmt
if(NOSOUND)
add_devilutionx_object_library(libdevilutionx_sound
effects_stubs.cpp
engine/sound_pool_stubs.cpp
engine/sound_stubs.cpp
)
target_link_dependencies(libdevilutionx_sound PUBLIC
DevilutionX::SDL
fmt::fmt
magic_enum::magic_enum
tl
unordered_dense::unordered_dense
@ -755,15 +761,15 @@ if(NOSOUND)
libdevilutionx_random
libdevilutionx_sdl2_to_1_2_backports
)
else()
add_devilutionx_object_library(libdevilutionx_sound
effects.cpp
engine/sound_pool.cpp
engine/sound.cpp
utils/soundsample.cpp
)
if(USE_SDL3)
target_link_dependencies(libdevilutionx_sound PUBLIC
else()
add_devilutionx_object_library(libdevilutionx_sound
effects.cpp
engine/sound_pool.cpp
engine/sound.cpp
utils/soundsample.cpp
)
if(USE_SDL3)
target_link_dependencies(libdevilutionx_sound PUBLIC
SDL3_mixer::SDL3_mixer
)
else()

340
Source/controls/accessibility_keys.cpp

@ -0,0 +1,340 @@
/**
* @file controls/accessibility_keys.cpp
*
* UI accessibility key handlers and action-guard helpers.
*/
#include "controls/accessibility_keys.hpp"
#include <algorithm>
#include <array>
#include <cstdint>
#include <string>
#include <fmt/format.h>
#include "control/control.hpp"
#ifdef USE_SDL3
#include <SDL3/SDL_keycode.h>
#else
#include <SDL.h>
#endif
#include "controls/plrctrls.h"
#include "utils/sdl_compat.h"
#include "diablo.h"
#include "gamemenu.h"
#include "help.h"
#include "inv.h"
#include "levels/gendung.h"
#include "levels/setmaps.h"
#include "minitext.h"
#include "options.h"
#include "panels/charpanel.hpp"
#include "panels/partypanel.hpp"
#include "panels/spell_book.hpp"
#include "panels/spell_list.hpp"
#include "player.h"
#include "qol/chatlog.h"
#include "qol/stash.h"
#include "quests.h"
#include "stores.h"
#include "utils/format_int.hpp"
#include "utils/language.h"
#include "utils/screen_reader.hpp"
#include "utils/str_cat.hpp"
namespace devilution {
namespace {
/** Computes a rounded percentage (0--100) from a current and maximum value. */
int ComputePercentage(int current, int maximum)
{
const int clamped = std::max(current, 0);
int percent = static_cast<int>((static_cast<int64_t>(clamped) * 100 + maximum / 2) / maximum);
return std::clamp(percent, 0, 100);
}
} // namespace
void SpeakPlayerHealthPercentageKeyPressed()
{
if (!CanPlayerTakeAction())
return;
if (MyPlayer == nullptr)
return;
const SDL_Keymod modState = SDL_GetModState();
const bool speakMana = (modState & SDL_KMOD_SHIFT) != 0;
if (speakMana) {
if (MyPlayer->_pMaxMana <= 0)
return;
SpeakText(fmt::format("{:d}%", ComputePercentage(MyPlayer->_pMana, MyPlayer->_pMaxMana)), /*force=*/true);
return;
}
if (MyPlayer->_pMaxHP <= 0)
return;
SpeakText(fmt::format("{:d}%", ComputePercentage(MyPlayer->_pHitPoints, MyPlayer->_pMaxHP)), /*force=*/true);
}
void SpeakExperienceToNextLevelKeyPressed()
{
if (!CanPlayerTakeAction())
return;
if (MyPlayer == nullptr)
return;
const Player &myPlayer = *MyPlayer;
if (myPlayer.isMaxCharacterLevel()) {
SpeakText(_("Max level."), /*force=*/true);
return;
}
const uint32_t nextExperienceThreshold = myPlayer.getNextExperienceThreshold();
const uint32_t currentExperience = myPlayer._pExperience;
const uint32_t remainingExperience = currentExperience >= nextExperienceThreshold ? 0 : nextExperienceThreshold - currentExperience;
const int nextLevel = myPlayer.getCharacterLevel() + 1;
SpeakText(
fmt::format(fmt::runtime(_("{:s} to Level {:d}")), FormatInteger(remainingExperience), nextLevel),
/*force=*/true);
}
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);
}
void SpeakCurrentLocationKeyPressed()
{
if (!CanPlayerTakeAction())
return;
SpeakText(BuildCurrentLocationForSpeech(), /*force=*/true);
}
void InventoryKeyPressed()
{
if (IsPlayerInStore())
return;
invflag = !invflag;
if (!IsLeftPanelOpen() && CanPanelsCoverView()) {
if (!invflag) { // We closed the inventory
if (MousePosition.x < 480 && MousePosition.y < GetMainPanel().position.y) {
SetCursorPos(MousePosition + Displacement { 160, 0 });
}
} else if (!SpellbookFlag) { // We opened the inventory
if (MousePosition.x > 160 && MousePosition.y < GetMainPanel().position.y) {
SetCursorPos(MousePosition - Displacement { 160, 0 });
}
}
}
SpellbookFlag = false;
CloseGoldWithdraw();
CloseStash();
if (invflag)
FocusOnInventory();
}
void CharacterSheetKeyPressed()
{
if (IsPlayerInStore())
return;
if (!IsRightPanelOpen() && CanPanelsCoverView()) {
if (CharFlag) { // We are closing the character sheet
if (MousePosition.x > 160 && MousePosition.y < GetMainPanel().position.y) {
SetCursorPos(MousePosition - Displacement { 160, 0 });
}
} else if (!QuestLogIsOpen) { // We opened the character sheet
if (MousePosition.x < 480 && MousePosition.y < GetMainPanel().position.y) {
SetCursorPos(MousePosition + Displacement { 160, 0 });
}
}
}
ToggleCharPanel();
}
void PartyPanelSideToggleKeyPressed()
{
PartySidePanelOpen = !PartySidePanelOpen;
}
void QuestLogKeyPressed()
{
if (IsPlayerInStore())
return;
if (!QuestLogIsOpen) {
StartQuestlog();
} else {
QuestLogIsOpen = false;
}
if (!IsRightPanelOpen() && CanPanelsCoverView()) {
if (!QuestLogIsOpen) { // We closed the quest log
if (MousePosition.x > 160 && MousePosition.y < GetMainPanel().position.y) {
SetCursorPos(MousePosition - Displacement { 160, 0 });
}
} else if (!CharFlag) { // We opened the quest log
if (MousePosition.x < 480 && MousePosition.y < GetMainPanel().position.y) {
SetCursorPos(MousePosition + Displacement { 160, 0 });
}
}
}
CloseCharPanel();
CloseGoldWithdraw();
CloseStash();
}
void SpeakSelectedSpeedbookSpell()
{
for (const auto &spellListItem : GetSpellListItems()) {
if (spellListItem.isSelected) {
SpeakText(pgettext("spell", GetSpellData(spellListItem.id).sNameText), /*force=*/true);
return;
}
}
SpeakText(_("No spell selected."), /*force=*/true);
}
void DisplaySpellsKeyPressed()
{
if (IsPlayerInStore())
return;
CloseCharPanel();
QuestLogIsOpen = false;
CloseInventory();
SpellbookFlag = false;
if (!SpellSelectFlag) {
DoSpeedBook();
SpeakSelectedSpeedbookSpell();
} else {
SpellSelectFlag = false;
}
LastPlayerAction = PlayerActionType::None;
}
void SpellBookKeyPressed()
{
if (IsPlayerInStore())
return;
SpellbookFlag = !SpellbookFlag;
if (SpellbookFlag && MyPlayer != nullptr) {
const Player &player = *MyPlayer;
if (IsValidSpell(player._pRSpell)) {
SpeakText(pgettext("spell", GetSpellData(player._pRSpell).sNameText), /*force=*/true);
} else {
SpeakText(_("No spell selected."), /*force=*/true);
}
}
if (!IsLeftPanelOpen() && CanPanelsCoverView()) {
if (!SpellbookFlag) { // We closed the spellbook
if (MousePosition.x < 480 && MousePosition.y < GetMainPanel().position.y) {
SetCursorPos(MousePosition + Displacement { 160, 0 });
}
} else if (!invflag) { // We opened the spellbook
if (MousePosition.x > 160 && MousePosition.y < GetMainPanel().position.y) {
SetCursorPos(MousePosition - Displacement { 160, 0 });
}
}
}
CloseInventory();
}
void CycleSpellHotkeys(bool next)
{
if (MyPlayer == nullptr)
return;
StaticVector<size_t, NumHotkeys> validHotKeyIndexes;
std::optional<size_t> currentIndex;
for (size_t slot = 0; slot < NumHotkeys; slot++) {
if (!IsValidSpeedSpell(slot))
continue;
if (MyPlayer->_pRSpell == MyPlayer->_pSplHotKey[slot] && MyPlayer->_pRSplType == MyPlayer->_pSplTHotKey[slot]) {
// found current
currentIndex = validHotKeyIndexes.size();
}
validHotKeyIndexes.emplace_back(slot);
}
if (validHotKeyIndexes.empty())
return;
size_t newIndex;
if (!currentIndex) {
newIndex = next ? 0 : (validHotKeyIndexes.size() - 1);
} else if (next) {
newIndex = (*currentIndex == validHotKeyIndexes.size() - 1) ? 0 : (*currentIndex + 1);
} else {
newIndex = *currentIndex == 0 ? (validHotKeyIndexes.size() - 1) : (*currentIndex - 1);
}
ToggleSpell(validHotKeyIndexes[newIndex]);
}
bool IsPlayerDead()
{
if (MyPlayer == nullptr)
return true;
return MyPlayer->_pmode == PM_DEATH || MyPlayerIsDead;
}
bool IsGameRunning()
{
return PauseMode != 2;
}
bool CanPlayerTakeAction()
{
return !IsPlayerDead() && IsGameRunning();
}
bool CanAutomapBeToggledOff()
{
return !QuestLogIsOpen && !IsWithdrawGoldOpen && !IsStashOpen && !CharFlag
&& !SpellbookFlag && !invflag && !isGameMenuOpen && !qtextflag && !SpellSelectFlag
&& !ChatLogFlag && !HelpFlag;
}
} // namespace devilution

30
Source/controls/accessibility_keys.hpp

@ -0,0 +1,30 @@
/**
* @file controls/accessibility_keys.hpp
*
* UI accessibility key handlers and action-guard helpers.
*/
#pragma once
#include <string>
namespace devilution {
void SpeakPlayerHealthPercentageKeyPressed();
void SpeakExperienceToNextLevelKeyPressed();
std::string BuildCurrentLocationForSpeech();
void SpeakCurrentLocationKeyPressed();
void InventoryKeyPressed();
void CharacterSheetKeyPressed();
void PartyPanelSideToggleKeyPressed();
void QuestLogKeyPressed();
void SpeakSelectedSpeedbookSpell();
void DisplaySpellsKeyPressed();
void SpellBookKeyPressed();
void CycleSpellHotkeys(bool next);
bool IsPlayerDead();
bool IsGameRunning();
bool CanPlayerTakeAction();
bool CanAutomapBeToggledOff();
} // namespace devilution

348
Source/controls/town_npc_nav.cpp

@ -0,0 +1,348 @@
/**
* @file controls/town_npc_nav.cpp
*
* Town NPC navigation for accessibility.
*/
#include "controls/town_npc_nav.hpp"
#include <algorithm>
#include <array>
#include <cstdint>
#include <string>
#include <vector>
#include <fmt/format.h>
#include "controls/accessibility_keys.hpp"
#include "controls/plrctrls.h"
#include "diablo.h"
#include "engine/path.h"
#include "help.h"
#include "levels/gendung.h"
#include "levels/tile_properties.hpp"
#include "multi.h"
#include "options.h"
#include "player.h"
#include "qol/chatlog.h"
#include "stores.h"
#include "towners.h"
#include "utils/language.h"
#include "utils/screen_reader.hpp"
#include "utils/str_cat.hpp"
#include "utils/walk_path_speech.hpp"
namespace devilution {
namespace {
std::vector<int> TownNpcOrder;
int SelectedTownNpc = -1;
int AutoWalkTownNpcTarget = -1;
void ResetTownNpcSelection()
{
TownNpcOrder.clear();
SelectedTownNpc = -1;
}
void RefreshTownNpcOrder(bool selectFirst = false)
{
TownNpcOrder.clear();
if (leveltype != DTYPE_TOWN)
return;
if (MyPlayer == nullptr) {
SelectedTownNpc = -1;
return;
}
const Point playerPosition = MyPlayer->position.future;
for (size_t i = 0; i < GetNumTowners(); ++i) {
const Towner &towner = Towners[i];
if (!IsTownerPresent(towner._ttype))
continue;
if (towner._ttype == TOWN_COW)
continue;
TownNpcOrder.push_back(static_cast<int>(i));
}
if (TownNpcOrder.empty()) {
SelectedTownNpc = -1;
return;
}
std::sort(TownNpcOrder.begin(), TownNpcOrder.end(), [&playerPosition](int a, int b) {
const Towner &townerA = Towners[a];
const Towner &townerB = Towners[b];
const int distanceA = playerPosition.WalkingDistance(townerA.position);
const int distanceB = playerPosition.WalkingDistance(townerB.position);
if (distanceA != distanceB)
return distanceA < distanceB;
return townerA.name < townerB.name;
});
if (selectFirst) {
SelectedTownNpc = TownNpcOrder.front();
return;
}
const auto it = std::find(TownNpcOrder.begin(), TownNpcOrder.end(), SelectedTownNpc);
if (it == TownNpcOrder.end())
SelectedTownNpc = TownNpcOrder.front();
}
void EnsureTownNpcOrder()
{
if (leveltype != DTYPE_TOWN) {
ResetTownNpcSelection();
return;
}
if (TownNpcOrder.empty()) {
RefreshTownNpcOrder(true);
return;
}
if (SelectedTownNpc < 0 || SelectedTownNpc >= static_cast<int>(GetNumTowners())) {
RefreshTownNpcOrder(true);
return;
}
const auto it = std::find(TownNpcOrder.begin(), TownNpcOrder.end(), SelectedTownNpc);
if (it == TownNpcOrder.end())
SelectedTownNpc = TownNpcOrder.front();
}
void SelectTownNpcRelative(int delta)
{
if (!IsTownNpcActionAllowed())
return;
EnsureTownNpcOrder();
if (TownNpcOrder.empty()) {
SpeakText(_("No town NPCs found."), true);
return;
}
auto it = std::find(TownNpcOrder.begin(), TownNpcOrder.end(), SelectedTownNpc);
int currentIndex = (it != TownNpcOrder.end()) ? static_cast<int>(it - TownNpcOrder.begin()) : 0;
const int size = static_cast<int>(TownNpcOrder.size());
int newIndex = (currentIndex + delta) % size;
if (newIndex < 0)
newIndex += size;
SelectedTownNpc = TownNpcOrder[static_cast<size_t>(newIndex)];
SpeakSelectedTownNpc();
}
} // namespace
bool IsTownNpcActionAllowed()
{
return CanPlayerTakeAction()
&& leveltype == DTYPE_TOWN
&& !IsPlayerInStore()
&& !ChatLogFlag
&& !HelpFlag;
}
void SpeakSelectedTownNpc()
{
EnsureTownNpcOrder();
if (SelectedTownNpc < 0 || SelectedTownNpc >= static_cast<int>(GetNumTowners())) {
SpeakText(_("No NPC selected."), true);
return;
}
if (MyPlayer == nullptr) {
SpeakText(_("No NPC selected."), true);
return;
}
const Towner &towner = Towners[SelectedTownNpc];
const Point playerPosition = MyPlayer->position.future;
const int distance = playerPosition.WalkingDistance(towner.position);
std::string msg;
StrAppend(msg, towner.name);
StrAppend(msg, "\n", _("Distance: "), distance);
StrAppend(msg, "\n", _("Position: "), towner.position.x, ", ", towner.position.y);
SpeakText(msg, true);
}
void SelectNextTownNpcKeyPressed()
{
SelectTownNpcRelative(+1);
}
void SelectPreviousTownNpcKeyPressed()
{
SelectTownNpcRelative(-1);
}
void GoToSelectedTownNpcKeyPressed()
{
if (!IsTownNpcActionAllowed())
return;
EnsureTownNpcOrder();
if (SelectedTownNpc < 0 || SelectedTownNpc >= static_cast<int>(GetNumTowners())) {
SpeakText(_("No NPC selected."), true);
return;
}
const Towner &towner = Towners[SelectedTownNpc];
std::string msg;
StrAppend(msg, _("Going to: "), towner.name);
SpeakText(msg, true);
AutoWalkTownNpcTarget = SelectedTownNpc;
UpdateAutoWalkTownNpc();
}
void UpdateAutoWalkTownNpc()
{
if (AutoWalkTownNpcTarget < 0)
return;
if (leveltype != DTYPE_TOWN || IsPlayerInStore() || ChatLogFlag || HelpFlag) {
AutoWalkTownNpcTarget = -1;
return;
}
if (!CanPlayerTakeAction())
return;
if (MyPlayer == nullptr) {
AutoWalkTownNpcTarget = -1;
return;
}
if (MyPlayer->_pmode != PM_STAND)
return;
if (MyPlayer->walkpath[0] != WALK_NONE)
return;
if (MyPlayer->destAction != ACTION_NONE)
return;
if (AutoWalkTownNpcTarget >= static_cast<int>(GetNumTowners())) {
AutoWalkTownNpcTarget = -1;
SpeakText(_("No NPC selected."), true);
return;
}
const Towner &towner = Towners[AutoWalkTownNpcTarget];
if (!IsTownerPresent(towner._ttype) || towner._ttype == TOWN_COW) {
AutoWalkTownNpcTarget = -1;
SpeakText(_("No NPC selected."), true);
return;
}
Player &myPlayer = *MyPlayer;
const Point playerPosition = myPlayer.position.future;
if (playerPosition.WalkingDistance(towner.position) < 2) {
const int townerIdx = AutoWalkTownNpcTarget;
AutoWalkTownNpcTarget = -1;
NetSendCmdLocParam1(true, CMD_TALKXY, towner.position, static_cast<uint16_t>(townerIdx));
return;
}
constexpr size_t MaxAutoWalkPathLength = 512;
std::array<int8_t, MaxAutoWalkPathLength> path;
path.fill(WALK_NONE);
const int steps = FindPath(CanStep, [&myPlayer](Point position) { return PosOkPlayer(myPlayer, position); }, playerPosition, towner.position, path.data(), path.size());
if (steps == 0) {
AutoWalkTownNpcTarget = -1;
std::string error;
StrAppend(error, _("Can't find a path to: "), towner.name);
SpeakText(error, true);
return;
}
// FindPath returns 0 when it fails to find a usable path.
// The player walkpath buffer is MaxPathLengthPlayer, so keep segments strictly shorter.
if (steps < static_cast<int>(MaxPathLengthPlayer)) {
const int townerIdx = AutoWalkTownNpcTarget;
AutoWalkTownNpcTarget = -1;
NetSendCmdLocParam1(true, CMD_TALKXY, towner.position, static_cast<uint16_t>(townerIdx));
return;
}
const int segmentSteps = std::min(steps - 1, static_cast<int>(MaxPathLengthPlayer - 1));
const Point waypoint = PositionAfterWalkPathSteps(playerPosition, path.data(), segmentSteps);
NetSendCmdLoc(MyPlayerId, true, CMD_WALKXY, waypoint);
}
void ResetAutoWalkTownNpc()
{
AutoWalkTownNpcTarget = -1;
}
void ListTownNpcsKeyPressed()
{
if (leveltype != DTYPE_TOWN) {
ResetTownNpcSelection();
SpeakText(_("Not in town."), true);
return;
}
if (IsPlayerInStore())
return;
std::vector<const Towner *> townNpcs;
std::vector<const Towner *> cows;
townNpcs.reserve(Towners.size());
cows.reserve(Towners.size());
if (MyPlayer == nullptr) {
SpeakText(_("No town NPCs found."), true);
return;
}
const Point playerPosition = MyPlayer->position.future;
for (const Towner &towner : Towners) {
if (!IsTownerPresent(towner._ttype))
continue;
if (towner._ttype == TOWN_COW) {
cows.push_back(&towner);
continue;
}
townNpcs.push_back(&towner);
}
if (townNpcs.empty() && cows.empty()) {
ResetTownNpcSelection();
SpeakText(_("No town NPCs found."), true);
return;
}
std::sort(townNpcs.begin(), townNpcs.end(), [&playerPosition](const Towner *a, const Towner *b) {
const int distanceA = playerPosition.WalkingDistance(a->position);
const int distanceB = playerPosition.WalkingDistance(b->position);
if (distanceA != distanceB)
return distanceA < distanceB;
return a->name < b->name;
});
std::string output;
StrAppend(output, _("Town NPCs:"));
for (size_t i = 0; i < townNpcs.size(); ++i) {
StrAppend(output, "\n", i + 1, ". ", townNpcs[i]->name);
}
if (!cows.empty()) {
StrAppend(output, "\n", _("Cows: "), static_cast<int>(cows.size()));
}
RefreshTownNpcOrder(true);
if (SelectedTownNpc >= 0 && SelectedTownNpc < static_cast<int>(GetNumTowners())) {
const Towner &towner = Towners[SelectedTownNpc];
StrAppend(output, "\n", _("Selected: "), towner.name);
StrAppend(output, "\n", _("PageUp/PageDown: select. Home: go. End: repeat."));
}
const std::string_view exitKey = GetOptions().Keymapper.KeyNameForAction("SpeakNearestExit");
if (!exitKey.empty()) {
StrAppend(output, "\n", fmt::format(fmt::runtime(_("Cathedral entrance: press {:s}.")), exitKey));
}
SpeakText(output, true);
}
} // namespace devilution

19
Source/controls/town_npc_nav.hpp

@ -0,0 +1,19 @@
/**
* @file controls/town_npc_nav.hpp
*
* Town NPC navigation for accessibility.
*/
#pragma once
namespace devilution {
void SelectNextTownNpcKeyPressed();
void SelectPreviousTownNpcKeyPressed();
void GoToSelectedTownNpcKeyPressed();
void SpeakSelectedTownNpc();
void ListTownNpcsKeyPressed();
void UpdateAutoWalkTownNpc();
bool IsTownNpcActionAllowed();
void ResetAutoWalkTownNpc();
} // namespace devilution

2859
Source/controls/tracker.cpp

File diff suppressed because it is too large Load Diff

35
Source/controls/tracker.hpp

@ -0,0 +1,35 @@
/**
* @file controls/tracker.hpp
*
* Tracker system for accessibility: target cycling, pathfinding, and auto-walk.
*/
#pragma once
#include <cstdint>
namespace devilution {
enum class TrackerTargetCategory : uint8_t {
Items,
Chests,
Doors,
Shrines,
Objects,
Breakables,
Monsters,
DeadBodies,
Npcs,
Players,
DungeonEntrances,
Stairs,
QuestLocations,
Portals,
};
void TrackerPageUpKeyPressed();
void TrackerPageDownKeyPressed();
void TrackerHomeKeyPressed();
void UpdateAutoWalkTracker();
void ResetAutoWalkTracker();
} // namespace devilution

11960
Source/diablo.cpp

File diff suppressed because it is too large Load Diff

491
Source/utils/accessibility_announcements.cpp

@ -0,0 +1,491 @@
/**
* @file utils/accessibility_announcements.cpp
*
* Periodic accessibility announcements (low HP warning, durability, boss health,
* attackable monsters, interactable doors).
*/
#include "utils/accessibility_announcements.hpp"
#include <algorithm>
#include <array>
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <vector>
#include <fmt/format.h>
#ifdef USE_SDL3
#include <SDL3/SDL_timer.h>
#else
#include <SDL.h>
#endif
#include "controls/plrctrls.h"
#include "engine/sound.h"
#include "gamemenu.h"
#include "inv.h"
#include "items.h"
#include "levels/gendung.h"
#include "monster.h"
#include "objects.h"
#include "player.h"
#include "utils/is_of.hpp"
#include "utils/language.h"
#include "utils/log.hpp"
#include "utils/screen_reader.hpp"
#include "utils/str_cat.hpp"
#include "utils/string_or_view.hpp"
namespace devilution {
#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) {
LogWarn("Failed to load low HP warning sound from any of the expected paths.");
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
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 UpdateAttackableMonsterAnnouncements()
{
static std::optional<int> 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<int> bestId;
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;
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<int>(player._pdir);
const int d2 = static_cast<int>(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);
}
StringOrView DoorLabelForSpeech(const Object &door)
{
if (!door.isDoor())
return door.name();
// Catacombs doors are grates, so differentiate them for the screen reader / tracker.
if (IsAnyOf(door._otype, _object_id::OBJ_L2LDOOR, _object_id::OBJ_L2RDOOR)) {
if (door._oVar4 == DOOR_OPEN)
return _("Open Grate Door");
if (door._oVar4 == DOOR_CLOSED)
return _("Closed Grate Door");
if (door._oVar4 == DOOR_BLOCKED)
return _("Blocked Grate Door");
return _("Grate Door");
}
return door.name();
}
void UpdateInteractableDoorAnnouncements()
{
static std::optional<int> LastInteractableDoorId;
static std::optional<int> LastInteractableDoorState;
if (MyPlayer == nullptr) {
LastInteractableDoorId = std::nullopt;
LastInteractableDoorState = std::nullopt;
return;
}
if (leveltype == DTYPE_TOWN) {
LastInteractableDoorId = std::nullopt;
LastInteractableDoorState = std::nullopt;
return;
}
if (MyPlayerIsDead || MyPlayer->_pmode == PM_DEATH || MyPlayer->hasNoLife()) {
LastInteractableDoorId = std::nullopt;
LastInteractableDoorState = std::nullopt;
return;
}
if (InGameMenu() || invflag) {
LastInteractableDoorId = std::nullopt;
LastInteractableDoorState = std::nullopt;
return;
}
const Player &player = *MyPlayer;
const Point playerPosition = player.position.tile;
std::optional<int> bestId;
int bestRotations = 5;
int bestDistance = 0;
for (int dy = -1; dy <= 1; ++dy) {
for (int dx = -1; dx <= 1; ++dx) {
if (dx == 0 && dy == 0)
continue;
const Point pos = playerPosition + Displacement { dx, dy };
if (!InDungeonBounds(pos))
continue;
const int objectId = std::abs(dObject[pos.x][pos.y]) - 1;
if (objectId < 0 || objectId >= MAXOBJECTS)
continue;
const Object &door = Objects[objectId];
if (!door.isDoor() || !door.canInteractWith())
continue;
const int distance = playerPosition.WalkingDistance(door.position);
if (distance > 1)
continue;
const int d1 = static_cast<int>(player._pdir);
const int d2 = static_cast<int>(GetDirection(playerPosition, door.position));
int rotations = std::abs(d1 - d2);
if (rotations > 4)
rotations = 4 - (rotations % 4);
if (!bestId || rotations < bestRotations || (rotations == bestRotations && distance < bestDistance)
|| (rotations == bestRotations && distance == bestDistance && objectId < *bestId)) {
bestRotations = rotations;
bestDistance = distance;
bestId = objectId;
}
}
}
if (!bestId) {
LastInteractableDoorId = std::nullopt;
LastInteractableDoorState = std::nullopt;
return;
}
const Object &door = Objects[*bestId];
const int state = door._oVar4;
if (LastInteractableDoorId && LastInteractableDoorState && *LastInteractableDoorId == *bestId && *LastInteractableDoorState == state)
return;
LastInteractableDoorId = *bestId;
LastInteractableDoorState = state;
const StringOrView label = DoorLabelForSpeech(door);
if (!label.empty())
SpeakText(label.str(), /*force=*/true);
}
} // namespace devilution

22
Source/utils/accessibility_announcements.hpp

@ -0,0 +1,22 @@
/**
* @file utils/accessibility_announcements.hpp
*
* Periodic accessibility announcements (low HP warning, durability, boss health,
* attackable monsters, interactable doors).
*/
#pragma once
#include "utils/string_or_view.hpp"
namespace devilution {
struct Object;
void UpdatePlayerLowHpWarningSound();
void UpdateLowDurabilityWarnings();
void UpdateBossHealthAnnouncements();
void UpdateAttackableMonsterAnnouncements();
StringOrView DoorLabelForSpeech(const Object &door);
void UpdateInteractableDoorAnnouncements();
} // namespace devilution

685
Source/utils/navigation_speech.cpp

@ -0,0 +1,685 @@
/**
* @file utils/navigation_speech.cpp
*
* Navigation speech: exit/stairs/portal/unexplored speech and keyboard walk keys.
*/
#include "utils/navigation_speech.hpp"
#include <algorithm>
#include <array>
#include <cstdint>
#include <optional>
#include <queue>
#include <string>
#include <vector>
#include <fmt/format.h>
#ifdef USE_SDL3
#include <SDL3/SDL_keycode.h>
#else
#include <SDL.h>
#endif
#include "automap.h"
#include "control/control.hpp"
#include "controls/accessibility_keys.hpp"
#include "controls/plrctrls.h"
#include "diablo.h"
#include "help.h"
#include "inv.h"
#include "levels/gendung.h"
#include "levels/setmaps.h"
#include "levels/tile_properties.hpp"
#include "levels/trigs.h"
#include "minitext.h"
#include "missiles.h"
#include "multi.h"
#include "player.h"
#include "portal.h"
#include "qol/chatlog.h"
#include "qol/stash.h"
#include "quests.h"
#include "stores.h"
#include "utils/language.h"
#include "utils/screen_reader.hpp"
#include "utils/str_cat.hpp"
#include "utils/sdl_compat.h"
#include "utils/walk_path_speech.hpp"
namespace devilution {
namespace {
std::optional<Point> FindNearestUnexploredTile(Point startPosition)
{
if (!InDungeonBounds(startPosition))
return std::nullopt;
std::array<bool, MAXDUNX * MAXDUNY> visited {};
std::queue<Point> queue;
const auto enqueue = [&](Point position) {
if (!InDungeonBounds(position))
return;
const size_t index = static_cast<size_t>(position.x) + static_cast<size_t>(position.y) * MAXDUNX;
if (visited[index])
return;
if (!IsTileWalkable(position, /*ignoreDoors=*/true))
return;
visited[index] = true;
queue.push(position);
};
enqueue(startPosition);
constexpr std::array<Direction, 4> Neighbors = {
Direction::NorthEast,
Direction::SouthWest,
Direction::SouthEast,
Direction::NorthWest,
};
while (!queue.empty()) {
const Point position = queue.front();
queue.pop();
if (!HasAnyOf(dFlags[position.x][position.y], DungeonFlag::Explored))
return position;
for (const Direction dir : Neighbors) {
enqueue(position + dir);
}
}
return std::nullopt;
}
std::optional<int> LockedTownDungeonTriggerIndex;
std::optional<int> FindDefaultTownDungeonTriggerIndex(const std::vector<int> &candidates)
{
for (const int index : candidates) {
if (trigs[index]._tmsg == WM_DIABNEXTLVL)
return index;
}
if (!candidates.empty())
return candidates.front();
return std::nullopt;
}
std::optional<int> FindLockedTownDungeonTriggerIndex(const std::vector<int> &candidates)
{
if (!LockedTownDungeonTriggerIndex)
return std::nullopt;
if (std::find(candidates.begin(), candidates.end(), *LockedTownDungeonTriggerIndex) != candidates.end())
return *LockedTownDungeonTriggerIndex;
return std::nullopt;
}
std::optional<int> FindNextTownDungeonTriggerIndex(const std::vector<int> &candidates, int current)
{
if (candidates.empty())
return std::nullopt;
const auto it = std::find(candidates.begin(), candidates.end(), current);
if (it == candidates.end())
return candidates.front();
if (std::next(it) == candidates.end())
return candidates.front();
return *std::next(it);
}
std::optional<int> FindNearestTriggerIndexWithMessage(int message)
{
if (numtrigs <= 0 || MyPlayer == nullptr)
return std::nullopt;
const Point playerPosition = MyPlayer->position.future;
std::optional<int> bestIndex;
int bestDistance = 0;
for (int i = 0; i < numtrigs; ++i) {
if (trigs[i]._tmsg != message)
continue;
const Point triggerPosition { trigs[i].position.x, trigs[i].position.y };
const int distance = playerPosition.WalkingDistance(triggerPosition);
if (!bestIndex || distance < bestDistance) {
bestIndex = i;
bestDistance = distance;
}
}
return bestIndex;
}
std::optional<Point> FindNearestTownPortalOnCurrentLevel()
{
if (MyPlayer == nullptr || leveltype == DTYPE_TOWN)
return std::nullopt;
const Point playerPosition = MyPlayer->position.future;
const int currentLevel = setlevel ? static_cast<int>(setlvlnum) : currlevel;
std::optional<Point> bestPosition;
int bestDistance = 0;
for (int i = 0; i < MAXPORTAL; ++i) {
const Portal &portal = Portals[i];
if (!portal.open)
continue;
if (portal.setlvl != setlevel)
continue;
if (portal.level != currentLevel)
continue;
const int distance = playerPosition.WalkingDistance(portal.position);
if (!bestPosition || distance < bestDistance) {
bestPosition = portal.position;
bestDistance = distance;
}
}
return bestPosition;
}
struct TownPortalInTown {
int portalIndex;
Point position;
int distance;
};
std::optional<TownPortalInTown> FindNearestTownPortalInTown()
{
if (MyPlayer == nullptr || leveltype != DTYPE_TOWN)
return std::nullopt;
const Point playerPosition = MyPlayer->position.future;
std::optional<TownPortalInTown> best;
int bestDistance = 0;
for (const Missile &missile : Missiles) {
if (missile._mitype != MissileID::TownPortal)
continue;
if (missile._misource < 0 || missile._misource >= MAXPORTAL)
continue;
if (!Portals[missile._misource].open)
continue;
const Point portalPosition = missile.position.tile;
const int distance = playerPosition.WalkingDistance(portalPosition);
if (!best || distance < bestDistance) {
best = TownPortalInTown {
.portalIndex = missile._misource,
.position = portalPosition,
.distance = distance,
};
bestDistance = distance;
}
}
return best;
}
struct QuestSetLevelEntrance {
_setlevels questLevel;
Point entrancePosition;
int distance;
};
std::optional<QuestSetLevelEntrance> FindNearestQuestSetLevelEntranceOnCurrentLevel()
{
if (MyPlayer == nullptr || setlevel)
return std::nullopt;
const Point playerPosition = MyPlayer->position.future;
std::optional<QuestSetLevelEntrance> best;
int bestDistance = 0;
for (const Quest &quest : Quests) {
if (quest._qslvl == SL_NONE)
continue;
if (quest._qactive == QUEST_NOTAVAIL)
continue;
if (quest._qlevel != currlevel)
continue;
if (!InDungeonBounds(quest.position))
continue;
const int distance = playerPosition.WalkingDistance(quest.position);
if (!best || distance < bestDistance) {
best = QuestSetLevelEntrance {
.questLevel = quest._qslvl,
.entrancePosition = quest.position,
.distance = distance,
};
bestDistance = distance;
}
}
return best;
}
void SpeakNearestStairsKeyPressed(int triggerMessage)
{
if (!CanPlayerTakeAction())
return;
if (AutomapActive) {
SpeakText(_("Close the map first."), true);
return;
}
if (leveltype == DTYPE_TOWN) {
SpeakText(_("Not in a dungeon."), true);
return;
}
if (MyPlayer == nullptr)
return;
const std::optional<int> triggerIndex = FindNearestTriggerIndexWithMessage(triggerMessage);
if (!triggerIndex) {
SpeakText(_("No exits found."), true);
return;
}
const TriggerStruct &trigger = trigs[*triggerIndex];
const Point startPosition = MyPlayer->position.future;
const Point targetPosition { trigger.position.x, trigger.position.y };
std::string message;
const std::optional<std::vector<int8_t>> path = FindKeyboardWalkPathForSpeech(*MyPlayer, startPosition, targetPosition);
if (!path) {
AppendDirectionalFallback(message, targetPosition - startPosition);
} else {
AppendKeyboardWalkPathForSpeech(message, *path);
}
SpeakText(message, true);
}
void KeyboardWalkKeyPressed(Direction direction)
{
CancelAutoWalk();
if (!IsKeyboardWalkAllowed())
return;
if (MyPlayer == nullptr)
return;
NetSendCmdLoc(MyPlayerId, true, CMD_WALKXY, MyPlayer->position.future + direction);
}
} // namespace
std::string TriggerLabelForSpeech(const TriggerStruct &trigger)
{
switch (trigger._tmsg) {
case WM_DIABNEXTLVL:
if (leveltype == DTYPE_TOWN)
return std::string { _("Cathedral entrance") };
return std::string { _("Stairs down") };
case WM_DIABPREVLVL:
return std::string { _("Stairs up") };
case WM_DIABTOWNWARP:
switch (trigger._tlvl) {
case 5:
return fmt::format(fmt::runtime(_("Town warp to {:s}")), _("Catacombs"));
case 9:
return fmt::format(fmt::runtime(_("Town warp to {:s}")), _("Caves"));
case 13:
return fmt::format(fmt::runtime(_("Town warp to {:s}")), _("Hell"));
case 17:
return fmt::format(fmt::runtime(_("Town warp to {:s}")), _("Nest"));
case 21:
return fmt::format(fmt::runtime(_("Town warp to {:s}")), _("Crypt"));
default:
return fmt::format(fmt::runtime(_("Town warp to level {:d}")), trigger._tlvl);
}
case WM_DIABTWARPUP:
return std::string { _("Warp up") };
case WM_DIABRETOWN:
return std::string { _("Return to town") };
case WM_DIABWARPLVL:
return std::string { _("Warp") };
case WM_DIABSETLVL:
return std::string { _("Set level") };
case WM_DIABRTNLVL:
return std::string { _("Return level") };
default:
return std::string { _("Exit") };
}
}
std::string TownPortalLabelForSpeech(const Portal &portal)
{
if (portal.level <= 0)
return std::string { _("Town portal") };
if (portal.setlvl) {
const auto questLevel = static_cast<_setlevels>(portal.level);
const char *questLevelName = QuestLevelNames[questLevel];
if (questLevelName == nullptr || questLevelName[0] == '\0')
return std::string { _("Town portal to set level") };
return fmt::format(fmt::runtime(_(/* TRANSLATORS: {:s} is a set/quest level name. */ "Town portal to {:s}")), _(questLevelName));
}
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 (portal.ltype >= DTYPE_TOWN && portal.ltype <= DTYPE_LAST) {
dungeonStr = _(DungeonStrs[static_cast<size_t>(portal.ltype)]);
} else {
dungeonStr = _(/* TRANSLATORS: type of dungeon (i.e. Cathedral, Caves)*/ "None");
}
int floor = portal.level;
if (portal.ltype == DTYPE_CATACOMBS)
floor -= 4;
else if (portal.ltype == DTYPE_CAVES)
floor -= 8;
else if (portal.ltype == DTYPE_HELL)
floor -= 12;
else if (portal.ltype == DTYPE_NEST)
floor -= 16;
else if (portal.ltype == DTYPE_CRYPT)
floor -= 20;
if (floor > 0)
return fmt::format(fmt::runtime(_(/* TRANSLATORS: {:s} is a dungeon name and {:d} is a floor number. */ "Town portal to {:s} {:d}")), dungeonStr, floor);
return fmt::format(fmt::runtime(_(/* TRANSLATORS: {:s} is a dungeon name. */ "Town portal to {:s}")), dungeonStr);
}
std::vector<int> CollectTownDungeonTriggerIndices()
{
std::vector<int> result;
result.reserve(static_cast<size_t>(std::max(0, numtrigs)));
for (int i = 0; i < numtrigs; ++i) {
if (IsAnyOf(trigs[i]._tmsg, WM_DIABNEXTLVL, WM_DIABTOWNWARP))
result.push_back(i);
}
std::sort(result.begin(), result.end(), [](int a, int b) {
const TriggerStruct &ta = trigs[a];
const TriggerStruct &tb = trigs[b];
const int kindA = ta._tmsg == WM_DIABNEXTLVL ? 0 : (ta._tmsg == WM_DIABTOWNWARP ? 1 : 2);
const int kindB = tb._tmsg == WM_DIABNEXTLVL ? 0 : (tb._tmsg == WM_DIABTOWNWARP ? 1 : 2);
if (kindA != kindB)
return kindA < kindB;
if (ta._tmsg == WM_DIABTOWNWARP && tb._tmsg == WM_DIABTOWNWARP && ta._tlvl != tb._tlvl)
return ta._tlvl < tb._tlvl;
return a < b;
});
return result;
}
void SpeakNearestExitKeyPressed()
{
if (!CanPlayerTakeAction())
return;
if (AutomapActive) {
SpeakText(_("Close the map first."), true);
return;
}
if (MyPlayer == nullptr)
return;
const Point startPosition = MyPlayer->position.future;
const SDL_Keymod modState = SDL_GetModState();
const bool seekQuestEntrance = (modState & SDL_KMOD_SHIFT) != 0;
const bool cycleTownDungeon = (modState & SDL_KMOD_CTRL) != 0;
if (seekQuestEntrance) {
if (setlevel) {
const std::optional<int> triggerIndex = FindNearestTriggerIndexWithMessage(WM_DIABRTNLVL);
if (!triggerIndex) {
SpeakText(_("No quest exits found."), true);
return;
}
const TriggerStruct &trigger = trigs[*triggerIndex];
const Point targetPosition { trigger.position.x, trigger.position.y };
const std::optional<std::vector<int8_t>> path = FindKeyboardWalkPathForSpeech(*MyPlayer, startPosition, targetPosition);
std::string message = TriggerLabelForSpeech(trigger);
if (!message.empty())
message.append(": ");
if (!path)
AppendDirectionalFallback(message, targetPosition - startPosition);
else
AppendKeyboardWalkPathForSpeech(message, *path);
SpeakText(message, true);
return;
}
if (const std::optional<QuestSetLevelEntrance> entrance = FindNearestQuestSetLevelEntranceOnCurrentLevel(); entrance) {
const Point targetPosition = entrance->entrancePosition;
const std::optional<std::vector<int8_t>> path = FindKeyboardWalkPathForSpeech(*MyPlayer, startPosition, targetPosition);
std::string message { _(QuestLevelNames[entrance->questLevel]) };
message.append(": ");
if (!path)
AppendDirectionalFallback(message, targetPosition - startPosition);
else
AppendKeyboardWalkPathForSpeech(message, *path);
SpeakText(message, true);
return;
}
SpeakText(_("No quest entrances found."), true);
return;
}
if (leveltype == DTYPE_TOWN) {
const std::vector<int> dungeonCandidates = CollectTownDungeonTriggerIndices();
if (dungeonCandidates.empty()) {
SpeakText(_("No exits found."), true);
return;
}
if (cycleTownDungeon) {
if (dungeonCandidates.size() <= 1) {
SpeakText(_("No other dungeon entrances found."), true);
return;
}
const int current = LockedTownDungeonTriggerIndex.value_or(-1);
const std::optional<int> next = FindNextTownDungeonTriggerIndex(dungeonCandidates, current);
if (!next) {
SpeakText(_("No other dungeon entrances found."), true);
return;
}
LockedTownDungeonTriggerIndex = *next;
const std::string label = TriggerLabelForSpeech(trigs[*next]);
if (!label.empty())
SpeakText(label, true);
return;
}
const int triggerIndex = FindLockedTownDungeonTriggerIndex(dungeonCandidates)
.value_or(FindDefaultTownDungeonTriggerIndex(dungeonCandidates).value_or(dungeonCandidates.front()));
LockedTownDungeonTriggerIndex = triggerIndex;
const TriggerStruct &trigger = trigs[triggerIndex];
const Point targetPosition { trigger.position.x, trigger.position.y };
const std::optional<std::vector<int8_t>> path = FindKeyboardWalkPathForSpeech(*MyPlayer, startPosition, targetPosition);
std::string message = TriggerLabelForSpeech(trigger);
if (!message.empty())
message.append(": ");
if (!path)
AppendDirectionalFallback(message, targetPosition - startPosition);
else
AppendKeyboardWalkPathForSpeech(message, *path);
SpeakText(message, true);
return;
}
if (const std::optional<Point> portalPosition = FindNearestTownPortalOnCurrentLevel(); portalPosition) {
const std::optional<std::vector<int8_t>> path = FindKeyboardWalkPathForSpeech(*MyPlayer, startPosition, *portalPosition);
std::string message { _("Return to town") };
message.append(": ");
if (!path)
AppendDirectionalFallback(message, *portalPosition - startPosition);
else
AppendKeyboardWalkPathForSpeech(message, *path);
SpeakText(message, true);
return;
}
const std::optional<int> triggerIndex = FindNearestTriggerIndexWithMessage(WM_DIABPREVLVL);
if (!triggerIndex) {
SpeakText(_("No exits found."), true);
return;
}
const TriggerStruct &trigger = trigs[*triggerIndex];
const Point targetPosition { trigger.position.x, trigger.position.y };
const std::optional<std::vector<int8_t>> path = FindKeyboardWalkPathForSpeech(*MyPlayer, startPosition, targetPosition);
std::string message = TriggerLabelForSpeech(trigger);
if (!message.empty())
message.append(": ");
if (!path)
AppendDirectionalFallback(message, targetPosition - startPosition);
else
AppendKeyboardWalkPathForSpeech(message, *path);
SpeakText(message, true);
}
void SpeakNearestTownPortalInTownKeyPressed()
{
if (!CanPlayerTakeAction())
return;
if (AutomapActive) {
SpeakText(_("Close the map first."), true);
return;
}
if (leveltype != DTYPE_TOWN) {
SpeakText(_("Not in town."), true);
return;
}
if (MyPlayer == nullptr)
return;
const std::optional<TownPortalInTown> portal = FindNearestTownPortalInTown();
if (!portal) {
SpeakText(_("No town portals found."), true);
return;
}
const Point startPosition = MyPlayer->position.future;
const Point targetPosition = portal->position;
const std::optional<std::vector<int8_t>> path = FindKeyboardWalkPathForSpeech(*MyPlayer, startPosition, targetPosition);
std::string message = TownPortalLabelForSpeech(Portals[portal->portalIndex]);
message.append(": ");
if (!path)
AppendDirectionalFallback(message, targetPosition - startPosition);
else
AppendKeyboardWalkPathForSpeech(message, *path);
SpeakText(message, true);
}
void SpeakNearestStairsDownKeyPressed()
{
SpeakNearestStairsKeyPressed(WM_DIABNEXTLVL);
}
void SpeakNearestStairsUpKeyPressed()
{
SpeakNearestStairsKeyPressed(WM_DIABPREVLVL);
}
bool IsKeyboardWalkAllowed()
{
return CanPlayerTakeAction()
&& !InGameMenu()
&& !IsPlayerInStore()
&& !QuestLogIsOpen
&& !HelpFlag
&& !ChatLogFlag
&& !ChatFlag
&& !DropGoldFlag
&& !IsStashOpen
&& !IsWithdrawGoldOpen
&& !AutomapActive
&& !invflag
&& !CharFlag
&& !SpellbookFlag
&& !SpellSelectFlag
&& !qtextflag;
}
void KeyboardWalkNorthKeyPressed()
{
KeyboardWalkKeyPressed(Direction::NorthEast);
}
void KeyboardWalkSouthKeyPressed()
{
KeyboardWalkKeyPressed(Direction::SouthWest);
}
void KeyboardWalkEastKeyPressed()
{
KeyboardWalkKeyPressed(Direction::SouthEast);
}
void KeyboardWalkWestKeyPressed()
{
KeyboardWalkKeyPressed(Direction::NorthWest);
}
void SpeakNearestUnexploredTileKeyPressed()
{
if (!CanPlayerTakeAction())
return;
if (leveltype == DTYPE_TOWN) {
SpeakText(_("Not in a dungeon."), true);
return;
}
if (AutomapActive) {
SpeakText(_("Close the map first."), true);
return;
}
if (MyPlayer == nullptr)
return;
const Point startPosition = MyPlayer->position.future;
const std::optional<Point> target = FindNearestUnexploredTile(startPosition);
if (!target) {
SpeakText(_("No unexplored areas found."), true);
return;
}
const std::optional<std::vector<int8_t>> path = FindKeyboardWalkPathForSpeech(*MyPlayer, startPosition, *target);
std::string message;
if (!path)
AppendDirectionalFallback(message, *target - startPosition);
else
AppendKeyboardWalkPathForSpeech(message, *path);
SpeakText(message, true);
}
} // namespace devilution

31
Source/utils/navigation_speech.hpp

@ -0,0 +1,31 @@
/**
* @file utils/navigation_speech.hpp
*
* Navigation speech: exit/stairs/portal/unexplored speech and keyboard walk keys.
*/
#pragma once
#include <string>
#include <vector>
namespace devilution {
struct TriggerStruct;
struct Portal;
std::string TriggerLabelForSpeech(const TriggerStruct &trigger);
std::string TownPortalLabelForSpeech(const Portal &portal);
std::vector<int> CollectTownDungeonTriggerIndices();
void SpeakNearestExitKeyPressed();
void SpeakNearestTownPortalInTownKeyPressed();
void SpeakNearestStairsDownKeyPressed();
void SpeakNearestStairsUpKeyPressed();
void KeyboardWalkNorthKeyPressed();
void KeyboardWalkSouthKeyPressed();
void KeyboardWalkEastKeyPressed();
void KeyboardWalkWestKeyPressed();
void SpeakNearestUnexploredTileKeyPressed();
bool IsKeyboardWalkAllowed();
} // namespace devilution

604
Source/utils/walk_path_speech.cpp

@ -0,0 +1,604 @@
/**
* @file utils/walk_path_speech.cpp
*
* Walk-path helpers, PosOk variants, and BFS pathfinding for accessibility speech.
*/
#include "utils/walk_path_speech.hpp"
#include <algorithm>
#include <array>
#include <cstdint>
#include <optional>
#include <queue>
#include <string>
#include <string_view>
#include <vector>
#include "engine/path.h"
#include "levels/gendung.h"
#include "levels/tile_properties.hpp"
#include "monster.h"
#include "objects.h"
#include "player.h"
#include "utils/language.h"
#include "utils/str_cat.hpp"
namespace devilution {
Point NextPositionForWalkDirection(Point position, int8_t walkDir)
{
switch (walkDir) {
case WALK_NE:
return { position.x, position.y - 1 };
case WALK_NW:
return { position.x - 1, position.y };
case WALK_SE:
return { position.x + 1, position.y };
case WALK_SW:
return { position.x, position.y + 1 };
case WALK_N:
return { position.x - 1, position.y - 1 };
case WALK_E:
return { position.x + 1, position.y - 1 };
case WALK_S:
return { position.x + 1, position.y + 1 };
case WALK_W:
return { position.x - 1, position.y + 1 };
default:
return position;
}
}
Point PositionAfterWalkPathSteps(Point start, const int8_t *path, int steps)
{
Point position = start;
for (int i = 0; i < steps; ++i) {
position = NextPositionForWalkDirection(position, path[i]);
}
return position;
}
int8_t OppositeWalkDirection(int8_t walkDir)
{
switch (walkDir) {
case WALK_NE:
return WALK_SW;
case WALK_SW:
return WALK_NE;
case WALK_NW:
return WALK_SE;
case WALK_SE:
return WALK_NW;
case WALK_N:
return WALK_S;
case WALK_S:
return WALK_N;
case WALK_E:
return WALK_W;
case WALK_W:
return WALK_E;
default:
return WALK_NONE;
}
}
bool PosOkPlayerIgnoreDoors(const Player &player, Point position)
{
if (!InDungeonBounds(position))
return false;
if (!IsTileWalkable(position, /*ignoreDoors=*/true))
return false;
Player *otherPlayer = PlayerAtPosition(position);
if (otherPlayer != nullptr && otherPlayer != &player && !otherPlayer->hasNoLife())
return false;
if (dMonster[position.x][position.y] != 0) {
if (leveltype == DTYPE_TOWN)
return false;
if (dMonster[position.x][position.y] <= 0)
return false;
if (!Monsters[dMonster[position.x][position.y] - 1].hasNoLife())
return false;
}
return true;
}
bool IsTileWalkableForTrackerPath(Point position, bool ignoreDoors, bool ignoreBreakables)
{
Object *object = FindObjectAtPosition(position);
if (object != nullptr) {
if (ignoreDoors && object->isDoor()) {
return true;
}
if (ignoreBreakables && object->_oSolidFlag && object->IsBreakable()) {
return true;
}
if (object->_oSolidFlag) {
return false;
}
}
return IsTileNotSolid(position);
}
bool PosOkPlayerIgnoreMonsters(const Player &player, Point position)
{
if (!InDungeonBounds(position))
return false;
if (!IsTileWalkableForTrackerPath(position, /*ignoreDoors=*/false, /*ignoreBreakables=*/false))
return false;
Player *otherPlayer = PlayerAtPosition(position);
if (otherPlayer != nullptr && otherPlayer != &player && !otherPlayer->hasNoLife())
return false;
return true;
}
bool PosOkPlayerIgnoreDoorsAndMonsters(const Player &player, Point position)
{
if (!InDungeonBounds(position))
return false;
if (!IsTileWalkableForTrackerPath(position, /*ignoreDoors=*/true, /*ignoreBreakables=*/false))
return false;
Player *otherPlayer = PlayerAtPosition(position);
if (otherPlayer != nullptr && otherPlayer != &player && !otherPlayer->hasNoLife())
return false;
return true;
}
bool PosOkPlayerIgnoreDoorsMonstersAndBreakables(const Player &player, Point position)
{
if (!InDungeonBounds(position))
return false;
if (!IsTileWalkableForTrackerPath(position, /*ignoreDoors=*/true, /*ignoreBreakables=*/true))
return false;
Player *otherPlayer = PlayerAtPosition(position);
if (otherPlayer != nullptr && otherPlayer != &player && !otherPlayer->hasNoLife())
return false;
return true;
}
namespace {
using PosOkForSpeechFn = bool (*)(const Player &, Point);
template <size_t NumDirections>
std::optional<std::vector<int8_t>> FindKeyboardWalkPathForSpeechBfs(const Player &player, Point startPosition, Point destinationPosition, PosOkForSpeechFn posOk, const std::array<int8_t, NumDirections> &walkDirections, bool allowDiagonalSteps, bool allowDestinationNonWalkable)
{
if (!InDungeonBounds(startPosition) || !InDungeonBounds(destinationPosition))
return std::nullopt;
if (startPosition == destinationPosition)
return std::vector<int8_t> {};
std::array<bool, MAXDUNX * MAXDUNY> visited {};
std::array<int8_t, MAXDUNX * MAXDUNY> parentDir {};
parentDir.fill(WALK_NONE);
std::queue<Point> queue;
const auto indexOf = [](Point position) -> size_t {
return static_cast<size_t>(position.x) + static_cast<size_t>(position.y) * MAXDUNX;
};
const auto enqueue = [&](Point current, int8_t dir) {
const Point next = NextPositionForWalkDirection(current, dir);
if (!InDungeonBounds(next))
return;
const size_t idx = indexOf(next);
if (visited[idx])
return;
const bool ok = posOk(player, next);
if (ok) {
if (!CanStep(current, next))
return;
} else {
if (!allowDestinationNonWalkable || next != destinationPosition)
return;
}
visited[idx] = true;
parentDir[idx] = dir;
queue.push(next);
};
visited[indexOf(startPosition)] = true;
queue.push(startPosition);
const auto hasReachedDestination = [&]() -> bool {
return visited[indexOf(destinationPosition)];
};
while (!queue.empty() && !hasReachedDestination()) {
const Point current = queue.front();
queue.pop();
const Displacement delta = destinationPosition - current;
const int deltaAbsX = delta.deltaX >= 0 ? delta.deltaX : -delta.deltaX;
const int deltaAbsY = delta.deltaY >= 0 ? delta.deltaY : -delta.deltaY;
std::array<int8_t, 8> prioritizedDirs;
size_t prioritizedCount = 0;
const auto addUniqueDir = [&](int8_t dir) {
if (dir == WALK_NONE)
return;
for (size_t i = 0; i < prioritizedCount; ++i) {
if (prioritizedDirs[i] == dir)
return;
}
prioritizedDirs[prioritizedCount++] = dir;
};
const int8_t xDir = delta.deltaX > 0 ? WALK_SE : (delta.deltaX < 0 ? WALK_NW : WALK_NONE);
const int8_t yDir = delta.deltaY > 0 ? WALK_SW : (delta.deltaY < 0 ? WALK_NE : WALK_NONE);
if (allowDiagonalSteps && delta.deltaX != 0 && delta.deltaY != 0) {
const int8_t diagDir = delta.deltaX > 0 ? (delta.deltaY > 0 ? WALK_S : WALK_E) : (delta.deltaY > 0 ? WALK_W : WALK_N);
addUniqueDir(diagDir);
}
if (deltaAbsX >= deltaAbsY) {
addUniqueDir(xDir);
addUniqueDir(yDir);
} else {
addUniqueDir(yDir);
addUniqueDir(xDir);
}
for (const int8_t dir : walkDirections) {
addUniqueDir(dir);
}
for (size_t i = 0; i < prioritizedCount; ++i) {
enqueue(current, prioritizedDirs[i]);
}
}
if (!hasReachedDestination())
return std::nullopt;
std::vector<int8_t> path;
Point position = destinationPosition;
while (position != startPosition) {
const int8_t dir = parentDir[indexOf(position)];
if (dir == WALK_NONE)
return std::nullopt;
path.push_back(dir);
position = NextPositionForWalkDirection(position, OppositeWalkDirection(dir));
}
std::reverse(path.begin(), path.end());
return path;
}
std::optional<std::vector<int8_t>> FindKeyboardWalkPathForSpeechWithPosOk(const Player &player, Point startPosition, Point destinationPosition, PosOkForSpeechFn posOk, bool allowDestinationNonWalkable)
{
constexpr std::array<int8_t, 4> AxisDirections = {
WALK_NE,
WALK_SW,
WALK_SE,
WALK_NW,
};
constexpr std::array<int8_t, 8> AllDirections = {
WALK_NE,
WALK_SW,
WALK_SE,
WALK_NW,
WALK_N,
WALK_E,
WALK_S,
WALK_W,
};
if (const std::optional<std::vector<int8_t>> axisPath = FindKeyboardWalkPathForSpeechBfs(player, startPosition, destinationPosition, posOk, AxisDirections, /*allowDiagonalSteps=*/false, allowDestinationNonWalkable); axisPath) {
return axisPath;
}
return FindKeyboardWalkPathForSpeechBfs(player, startPosition, destinationPosition, posOk, AllDirections, /*allowDiagonalSteps=*/true, allowDestinationNonWalkable);
}
template <size_t NumDirections>
std::optional<std::vector<int8_t>> FindKeyboardWalkPathToClosestReachableForSpeechBfs(const Player &player, Point startPosition, Point destinationPosition, PosOkForSpeechFn posOk, const std::array<int8_t, NumDirections> &walkDirections, bool allowDiagonalSteps, Point &closestPosition)
{
if (!InDungeonBounds(startPosition) || !InDungeonBounds(destinationPosition))
return std::nullopt;
if (startPosition == destinationPosition) {
closestPosition = destinationPosition;
return std::vector<int8_t> {};
}
std::array<bool, MAXDUNX * MAXDUNY> visited {};
std::array<int8_t, MAXDUNX * MAXDUNY> parentDir {};
std::array<uint16_t, MAXDUNX * MAXDUNY> depth {};
parentDir.fill(WALK_NONE);
depth.fill(0);
std::queue<Point> queue;
const auto indexOf = [](Point position) -> size_t {
return static_cast<size_t>(position.x) + static_cast<size_t>(position.y) * MAXDUNX;
};
const auto enqueue = [&](Point current, int8_t dir) {
const Point next = NextPositionForWalkDirection(current, dir);
if (!InDungeonBounds(next))
return;
const size_t nextIdx = indexOf(next);
if (visited[nextIdx])
return;
if (!posOk(player, next))
return;
if (!CanStep(current, next))
return;
const size_t currentIdx = indexOf(current);
visited[nextIdx] = true;
parentDir[nextIdx] = dir;
depth[nextIdx] = static_cast<uint16_t>(depth[currentIdx] + 1);
queue.push(next);
};
const size_t startIdx = indexOf(startPosition);
visited[startIdx] = true;
queue.push(startPosition);
Point best = startPosition;
int bestDistance = startPosition.WalkingDistance(destinationPosition);
uint16_t bestDepth = 0;
const auto considerBest = [&](Point position) {
const int distance = position.WalkingDistance(destinationPosition);
const uint16_t posDepth = depth[indexOf(position)];
if (distance < bestDistance || (distance == bestDistance && posDepth < bestDepth)) {
best = position;
bestDistance = distance;
bestDepth = posDepth;
}
};
while (!queue.empty()) {
const Point current = queue.front();
queue.pop();
considerBest(current);
const Displacement delta = destinationPosition - current;
const int deltaAbsX = delta.deltaX >= 0 ? delta.deltaX : -delta.deltaX;
const int deltaAbsY = delta.deltaY >= 0 ? delta.deltaY : -delta.deltaY;
std::array<int8_t, 8> prioritizedDirs;
size_t prioritizedCount = 0;
const auto addUniqueDir = [&](int8_t dir) {
if (dir == WALK_NONE)
return;
for (size_t i = 0; i < prioritizedCount; ++i) {
if (prioritizedDirs[i] == dir)
return;
}
prioritizedDirs[prioritizedCount++] = dir;
};
const int8_t xDir = delta.deltaX > 0 ? WALK_SE : (delta.deltaX < 0 ? WALK_NW : WALK_NONE);
const int8_t yDir = delta.deltaY > 0 ? WALK_SW : (delta.deltaY < 0 ? WALK_NE : WALK_NONE);
if (allowDiagonalSteps && delta.deltaX != 0 && delta.deltaY != 0) {
const int8_t diagDir = delta.deltaX > 0 ? (delta.deltaY > 0 ? WALK_S : WALK_E) : (delta.deltaY > 0 ? WALK_W : WALK_N);
addUniqueDir(diagDir);
}
if (deltaAbsX >= deltaAbsY) {
addUniqueDir(xDir);
addUniqueDir(yDir);
} else {
addUniqueDir(yDir);
addUniqueDir(xDir);
}
for (const int8_t dir : walkDirections) {
addUniqueDir(dir);
}
for (size_t i = 0; i < prioritizedCount; ++i) {
enqueue(current, prioritizedDirs[i]);
}
}
closestPosition = best;
if (best == startPosition)
return std::vector<int8_t> {};
std::vector<int8_t> path;
Point position = best;
while (position != startPosition) {
const int8_t dir = parentDir[indexOf(position)];
if (dir == WALK_NONE)
return std::nullopt;
path.push_back(dir);
position = NextPositionForWalkDirection(position, OppositeWalkDirection(dir));
}
std::reverse(path.begin(), path.end());
return path;
}
} // namespace
std::optional<std::vector<int8_t>> FindKeyboardWalkPathForSpeech(const Player &player, Point startPosition, Point destinationPosition, bool allowDestinationNonWalkable)
{
return FindKeyboardWalkPathForSpeechWithPosOk(player, startPosition, destinationPosition, PosOkPlayerIgnoreDoors, allowDestinationNonWalkable);
}
std::optional<std::vector<int8_t>> FindKeyboardWalkPathForSpeechRespectingDoors(const Player &player, Point startPosition, Point destinationPosition, bool allowDestinationNonWalkable)
{
return FindKeyboardWalkPathForSpeechWithPosOk(player, startPosition, destinationPosition, PosOkPlayer, allowDestinationNonWalkable);
}
std::optional<std::vector<int8_t>> FindKeyboardWalkPathForSpeechIgnoringMonsters(const Player &player, Point startPosition, Point destinationPosition, bool allowDestinationNonWalkable)
{
return FindKeyboardWalkPathForSpeechWithPosOk(player, startPosition, destinationPosition, PosOkPlayerIgnoreDoorsAndMonsters, allowDestinationNonWalkable);
}
std::optional<std::vector<int8_t>> FindKeyboardWalkPathForSpeechRespectingDoorsIgnoringMonsters(const Player &player, Point startPosition, Point destinationPosition, bool allowDestinationNonWalkable)
{
return FindKeyboardWalkPathForSpeechWithPosOk(player, startPosition, destinationPosition, PosOkPlayerIgnoreMonsters, allowDestinationNonWalkable);
}
std::optional<std::vector<int8_t>> FindKeyboardWalkPathForSpeechLenient(const Player &player, Point startPosition, Point destinationPosition, bool allowDestinationNonWalkable)
{
return FindKeyboardWalkPathForSpeechWithPosOk(player, startPosition, destinationPosition, PosOkPlayerIgnoreDoorsMonstersAndBreakables, allowDestinationNonWalkable);
}
std::optional<std::vector<int8_t>> FindKeyboardWalkPathToClosestReachableForSpeech(const Player &player, Point startPosition, Point destinationPosition, Point &closestPosition)
{
constexpr std::array<int8_t, 4> AxisDirections = {
WALK_NE,
WALK_SW,
WALK_SE,
WALK_NW,
};
constexpr std::array<int8_t, 8> AllDirections = {
WALK_NE,
WALK_SW,
WALK_SE,
WALK_NW,
WALK_N,
WALK_E,
WALK_S,
WALK_W,
};
Point axisClosest;
const std::optional<std::vector<int8_t>> axisPath = FindKeyboardWalkPathToClosestReachableForSpeechBfs(player, startPosition, destinationPosition, PosOkPlayerIgnoreDoors, AxisDirections, /*allowDiagonalSteps=*/false, axisClosest);
Point diagClosest;
const std::optional<std::vector<int8_t>> diagPath = FindKeyboardWalkPathToClosestReachableForSpeechBfs(player, startPosition, destinationPosition, PosOkPlayerIgnoreDoors, AllDirections, /*allowDiagonalSteps=*/true, diagClosest);
if (!axisPath && !diagPath)
return std::nullopt;
if (!axisPath) {
closestPosition = diagClosest;
return diagPath;
}
if (!diagPath) {
closestPosition = axisClosest;
return axisPath;
}
const int axisDistance = axisClosest.WalkingDistance(destinationPosition);
const int diagDistance = diagClosest.WalkingDistance(destinationPosition);
if (diagDistance < axisDistance) {
closestPosition = diagClosest;
return diagPath;
}
closestPosition = axisClosest;
return axisPath;
}
void AppendKeyboardWalkPathForSpeech(std::string &message, const std::vector<int8_t> &path)
{
if (path.empty()) {
message.append(_("here"));
return;
}
bool any = false;
const auto appendPart = [&](std::string_view label, int distance) {
if (distance == 0)
return;
if (any)
message.append(", ");
StrAppend(message, label, " ", distance);
any = true;
};
const auto labelForWalkDirection = [](int8_t dir) -> std::string_view {
switch (dir) {
case WALK_NE:
return _("north");
case WALK_SW:
return _("south");
case WALK_SE:
return _("east");
case WALK_NW:
return _("west");
case WALK_N:
return _("northwest");
case WALK_E:
return _("northeast");
case WALK_S:
return _("southeast");
case WALK_W:
return _("southwest");
default:
return {};
}
};
int8_t currentDir = path.front();
int runLength = 1;
for (size_t i = 1; i < path.size(); ++i) {
if (path[i] == currentDir) {
++runLength;
continue;
}
const std::string_view label = labelForWalkDirection(currentDir);
if (!label.empty())
appendPart(label, runLength);
currentDir = path[i];
runLength = 1;
}
const std::string_view label = labelForWalkDirection(currentDir);
if (!label.empty())
appendPart(label, runLength);
if (!any)
message.append(_("here"));
}
void AppendDirectionalFallback(std::string &message, const Displacement &delta)
{
bool any = false;
const auto appendPart = [&](std::string_view label, int distance) {
if (distance == 0)
return;
if (any)
message.append(", ");
StrAppend(message, label, " ", distance);
any = true;
};
if (delta.deltaY < 0)
appendPart(_("north"), -delta.deltaY);
else if (delta.deltaY > 0)
appendPart(_("south"), delta.deltaY);
if (delta.deltaX > 0)
appendPart(_("east"), delta.deltaX);
else if (delta.deltaX < 0)
appendPart(_("west"), -delta.deltaX);
if (!any)
message.append(_("here"));
}
} // namespace devilution

44
Source/utils/walk_path_speech.hpp

@ -0,0 +1,44 @@
/**
* @file utils/walk_path_speech.hpp
*
* Walk-path helpers, PosOk variants, and BFS pathfinding for accessibility speech.
*/
#pragma once
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
#include "engine/displacement.hpp"
#include "engine/point.hpp"
namespace devilution {
struct Player;
// Walk direction helpers
Point NextPositionForWalkDirection(Point position, int8_t walkDir);
Point PositionAfterWalkPathSteps(Point start, const int8_t *path, int steps);
int8_t OppositeWalkDirection(int8_t walkDir);
// PosOk variants for pathfinding
bool PosOkPlayerIgnoreDoors(const Player &player, Point position);
bool IsTileWalkableForTrackerPath(Point position, bool ignoreDoors, bool ignoreBreakables);
bool PosOkPlayerIgnoreMonsters(const Player &player, Point position);
bool PosOkPlayerIgnoreDoorsAndMonsters(const Player &player, Point position);
bool PosOkPlayerIgnoreDoorsMonstersAndBreakables(const Player &player, Point position);
// BFS pathfinding for speech
std::optional<std::vector<int8_t>> FindKeyboardWalkPathForSpeech(const Player &player, Point startPosition, Point destinationPosition, bool allowDestinationNonWalkable = false);
std::optional<std::vector<int8_t>> FindKeyboardWalkPathForSpeechRespectingDoors(const Player &player, Point startPosition, Point destinationPosition, bool allowDestinationNonWalkable = false);
std::optional<std::vector<int8_t>> FindKeyboardWalkPathForSpeechIgnoringMonsters(const Player &player, Point startPosition, Point destinationPosition, bool allowDestinationNonWalkable = false);
std::optional<std::vector<int8_t>> FindKeyboardWalkPathForSpeechRespectingDoorsIgnoringMonsters(const Player &player, Point startPosition, Point destinationPosition, bool allowDestinationNonWalkable = false);
std::optional<std::vector<int8_t>> FindKeyboardWalkPathForSpeechLenient(const Player &player, Point startPosition, Point destinationPosition, bool allowDestinationNonWalkable = false);
std::optional<std::vector<int8_t>> FindKeyboardWalkPathToClosestReachableForSpeech(const Player &player, Point startPosition, Point destinationPosition, Point &closestPosition);
// Speech formatting
void AppendKeyboardWalkPathForSpeech(std::string &message, const std::vector<int8_t> &path);
void AppendDirectionalFallback(std::string &message, const Displacement &delta);
} // namespace devilution
Loading…
Cancel
Save