14 changed files with 8933 additions and 8589 deletions
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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…
Reference in new issue