Browse Source
Add Source/accessibility/ with four new modules: - speech.cpp/hpp: low-HP warnings, durability warnings, boss/monster HP announcements, interactable door announcements - tracker.cpp/hpp: TrackerTargetCategory, path-finding predicates, tracker navigation (cycle, navigate, auto-walk) - town_navigation.cpp/hpp: town NPC selection, listing, auto-walk - location_speech.cpp/hpp: BFS path-finding for speech, player status announcements, nearest-exit/stairs/portal speech, keyboard walk Wire the new modules into diablo.cpp via four include lines. Declare CanPlayerTakeAction() and CancelAutoWalk() in diablo.h. Add the four new source files to Source/CMakeLists.txt.pull/8486/head
11 changed files with 4165 additions and 1 deletions
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,49 @@ |
|||||||
|
#pragma once |
||||||
|
|
||||||
|
#include <cstdint> |
||||||
|
#include <optional> |
||||||
|
#include <string> |
||||||
|
#include <vector> |
||||||
|
|
||||||
|
#include "engine/displacement.hpp" |
||||||
|
#include "engine/point.hpp" |
||||||
|
|
||||||
|
namespace devilution { |
||||||
|
|
||||||
|
struct Player; |
||||||
|
|
||||||
|
// Path-finding utilities for speech/navigation (BFS over the dungeon grid).
|
||||||
|
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); |
||||||
|
|
||||||
|
void AppendKeyboardWalkPathForSpeech(std::string &message, const std::vector<int8_t> &path); |
||||||
|
void AppendDirectionalFallback(std::string &message, const Displacement &delta); |
||||||
|
|
||||||
|
// Key handler that speaks the nearest unexplored tile direction.
|
||||||
|
void SpeakNearestUnexploredTileKeyPressed(); |
||||||
|
|
||||||
|
[[nodiscard]] std::string BuildCurrentLocationForSpeech(); |
||||||
|
|
||||||
|
// Key handlers for player status announcements.
|
||||||
|
void SpeakPlayerHealthPercentageKeyPressed(); |
||||||
|
void SpeakExperienceToNextLevelKeyPressed(); |
||||||
|
void SpeakCurrentLocationKeyPressed(); |
||||||
|
|
||||||
|
// Key handlers for navigation.
|
||||||
|
void SpeakNearestExitKeyPressed(); |
||||||
|
void SpeakNearestTownPortalInTownKeyPressed(); |
||||||
|
void SpeakNearestStairsDownKeyPressed(); |
||||||
|
void SpeakNearestStairsUpKeyPressed(); |
||||||
|
|
||||||
|
// Keyboard directional walk.
|
||||||
|
bool IsKeyboardWalkAllowed(); |
||||||
|
void KeyboardWalkNorthKeyPressed(); |
||||||
|
void KeyboardWalkSouthKeyPressed(); |
||||||
|
void KeyboardWalkEastKeyPressed(); |
||||||
|
void KeyboardWalkWestKeyPressed(); |
||||||
|
|
||||||
|
} // namespace devilution
|
||||||
@ -0,0 +1,491 @@ |
|||||||
|
#include "accessibility/speech.hpp" |
||||||
|
|
||||||
|
#include <algorithm> |
||||||
|
#include <array> |
||||||
|
#include <cstdint> |
||||||
|
#include <memory> |
||||||
|
#include <optional> |
||||||
|
#include <string> |
||||||
|
#include <vector> |
||||||
|
|
||||||
|
#ifdef USE_SDL3 |
||||||
|
#include <SDL3/SDL_timer.h> |
||||||
|
#else |
||||||
|
#include <SDL.h> |
||||||
|
#endif |
||||||
|
|
||||||
|
#if !defined(USE_SDL3) && !defined(NOSOUND) |
||||||
|
#include <Aulib/Stream.h> |
||||||
|
#endif |
||||||
|
|
||||||
|
#include <fmt/format.h> |
||||||
|
|
||||||
|
#include "controls/plrctrls.h" |
||||||
|
#include "engine/sound.h" |
||||||
|
#include "inv.h" |
||||||
|
#include "options.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/screen_reader.hpp" |
||||||
|
#include "utils/str_cat.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) { |
||||||
|
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, *GetOptions().Audio.soundVolume); |
||||||
|
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); |
||||||
|
} |
||||||
|
|
||||||
|
[[nodiscard]] StringOrView DoorLabelForSpeech(const Object &door) |
||||||
|
{ |
||||||
|
if (!door.isDoor()) |
||||||
|
return door.name(); |
||||||
|
|
||||||
|
// Door state values are defined in `Source/objects.cpp` (DOOR_CLOSED=0, DOOR_OPEN=1, DOOR_BLOCKED=2).
|
||||||
|
constexpr int DoorClosed = 0; |
||||||
|
constexpr int DoorOpen = 1; |
||||||
|
constexpr int DoorBlocked = 2; |
||||||
|
|
||||||
|
// 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 == DoorOpen) |
||||||
|
return _("Open Grate Door"); |
||||||
|
if (door._oVar4 == DoorClosed) |
||||||
|
return _("Closed Grate Door"); |
||||||
|
if (door._oVar4 == DoorBlocked) |
||||||
|
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,17 @@ |
|||||||
|
#pragma once |
||||||
|
|
||||||
|
#include "utils/string_or_view.hpp" |
||||||
|
|
||||||
|
namespace devilution { |
||||||
|
|
||||||
|
struct Object; |
||||||
|
|
||||||
|
[[nodiscard]] StringOrView DoorLabelForSpeech(const Object &door); |
||||||
|
|
||||||
|
void UpdatePlayerLowHpWarningSound(); |
||||||
|
void UpdateLowDurabilityWarnings(); |
||||||
|
void UpdateBossHealthAnnouncements(); |
||||||
|
void UpdateAttackableMonsterAnnouncements(); |
||||||
|
void UpdateInteractableDoorAnnouncements(); |
||||||
|
|
||||||
|
} // namespace devilution
|
||||||
@ -0,0 +1,358 @@ |
|||||||
|
#include "accessibility/town_navigation.hpp" |
||||||
|
|
||||||
|
#include <algorithm> |
||||||
|
#include <array> |
||||||
|
#include <cstddef> |
||||||
|
#include <string> |
||||||
|
#include <vector> |
||||||
|
|
||||||
|
#include <fmt/format.h> |
||||||
|
|
||||||
|
#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" |
||||||
|
|
||||||
|
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; |
||||||
|
|
||||||
|
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(); |
||||||
|
} |
||||||
|
|
||||||
|
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; |
||||||
|
} |
||||||
|
|
||||||
|
} // 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; |
||||||
|
} |
||||||
|
|
||||||
|
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 CancelTownNpcAutoWalk() |
||||||
|
{ |
||||||
|
AutoWalkTownNpcTarget = -1; |
||||||
|
} |
||||||
|
|
||||||
|
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->_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 if the path length is equal to the maximum.
|
||||||
|
// 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 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()); |
||||||
|
|
||||||
|
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,15 @@ |
|||||||
|
#pragma once |
||||||
|
|
||||||
|
namespace devilution { |
||||||
|
|
||||||
|
bool IsTownNpcActionAllowed(); |
||||||
|
void SpeakSelectedTownNpc(); |
||||||
|
|
||||||
|
void SelectNextTownNpcKeyPressed(); |
||||||
|
void SelectPreviousTownNpcKeyPressed(); |
||||||
|
void GoToSelectedTownNpcKeyPressed(); |
||||||
|
void ListTownNpcsKeyPressed(); |
||||||
|
void UpdateAutoWalkTownNpc(); |
||||||
|
void CancelTownNpcAutoWalk(); |
||||||
|
|
||||||
|
} // namespace devilution
|
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,39 @@ |
|||||||
|
#pragma once |
||||||
|
|
||||||
|
#include <cstdint> |
||||||
|
#include <optional> |
||||||
|
#include <vector> |
||||||
|
|
||||||
|
#include "engine/point.hpp" |
||||||
|
|
||||||
|
namespace devilution { |
||||||
|
|
||||||
|
struct Player; |
||||||
|
|
||||||
|
enum class TrackerTargetCategory : uint8_t { |
||||||
|
Items, |
||||||
|
Chests, |
||||||
|
Doors, |
||||||
|
Shrines, |
||||||
|
Objects, |
||||||
|
Breakables, |
||||||
|
Monsters, |
||||||
|
DeadBodies, |
||||||
|
}; |
||||||
|
|
||||||
|
extern TrackerTargetCategory SelectedTrackerTargetCategory; |
||||||
|
extern TrackerTargetCategory AutoWalkTrackerTargetCategory; |
||||||
|
extern int AutoWalkTrackerTargetId; |
||||||
|
|
||||||
|
// Position-check predicates used by both tracker and location_speech path-finding.
|
||||||
|
bool PosOkPlayerIgnoreDoors(const Player &player, Point position); |
||||||
|
bool PosOkPlayerIgnoreMonsters(const Player &player, Point position); |
||||||
|
bool PosOkPlayerIgnoreDoorsAndMonsters(const Player &player, Point position); |
||||||
|
bool PosOkPlayerIgnoreDoorsMonstersAndBreakables(const Player &player, Point position); |
||||||
|
|
||||||
|
void CycleTrackerTargetKeyPressed(); |
||||||
|
void NavigateToTrackerTargetKeyPressed(); |
||||||
|
void AutoWalkToTrackerTargetKeyPressed(); |
||||||
|
void UpdateAutoWalkTracker(); |
||||||
|
|
||||||
|
} // namespace devilution
|
||||||
Loading…
Reference in new issue