#include "accessibility/tracker.hpp" #include #include #include #include #include #include #include #include #ifdef USE_SDL3 #include #else #include #endif #include #include "accessibility/location_speech.hpp" #include "accessibility/speech.hpp" #include "appfat.h" #include "automap.h" #include "controls/plrctrls.h" #include "diablo.h" #include "engine/path.h" #include "help.h" #include "items.h" #include "levels/gendung.h" #include "levels/tile_properties.hpp" #include "monster.h" #include "multi.h" #include "objects.h" #include "player.h" #include "qol/chatlog.h" #include "stores.h" #include "utils/is_of.hpp" #include "utils/language.h" #include "utils/screen_reader.hpp" #include "utils/sdl_compat.h" #include "utils/str_cat.hpp" namespace devilution { TrackerTargetCategory SelectedTrackerTargetCategory = TrackerTargetCategory::Items; TrackerTargetCategory AutoWalkTrackerTargetCategory = TrackerTargetCategory::Items; int AutoWalkTrackerTargetId = -1; namespace { /// Maximum Chebyshev distance (in tiles) at which the player is considered /// close enough to interact with a tracker target. constexpr int TrackerInteractDistanceTiles = 1; constexpr int TrackerCycleDistanceTiles = 12; int LockedTrackerItemId = -1; int LockedTrackerChestId = -1; int LockedTrackerDoorId = -1; int LockedTrackerShrineId = -1; int LockedTrackerObjectId = -1; int LockedTrackerBreakableId = -1; int LockedTrackerMonsterId = -1; int LockedTrackerDeadBodyId = -1; struct TrackerLevelKey { dungeon_type levelType; int currLevel; bool isSetLevel; int setLevelNum; }; std::optional LockedTrackerLevelKey; void ClearTrackerLocks() { LockedTrackerItemId = -1; LockedTrackerChestId = -1; LockedTrackerDoorId = -1; LockedTrackerShrineId = -1; LockedTrackerObjectId = -1; LockedTrackerBreakableId = -1; LockedTrackerMonsterId = -1; LockedTrackerDeadBodyId = -1; } void EnsureTrackerLocksMatchCurrentLevel() { const TrackerLevelKey current { .levelType = leveltype, .currLevel = currlevel, .isSetLevel = setlevel, .setLevelNum = setlvlnum, }; if (!LockedTrackerLevelKey || LockedTrackerLevelKey->levelType != current.levelType || LockedTrackerLevelKey->currLevel != current.currLevel || LockedTrackerLevelKey->isSetLevel != current.isSetLevel || LockedTrackerLevelKey->setLevelNum != current.setLevelNum) { ClearTrackerLocks(); LockedTrackerLevelKey = current; } } int &LockedTrackerTargetId(TrackerTargetCategory category) { switch (category) { case TrackerTargetCategory::Items: return LockedTrackerItemId; case TrackerTargetCategory::Chests: return LockedTrackerChestId; case TrackerTargetCategory::Doors: return LockedTrackerDoorId; case TrackerTargetCategory::Shrines: return LockedTrackerShrineId; case TrackerTargetCategory::Objects: return LockedTrackerObjectId; case TrackerTargetCategory::Breakables: return LockedTrackerBreakableId; case TrackerTargetCategory::Monsters: return LockedTrackerMonsterId; case TrackerTargetCategory::DeadBodies: return LockedTrackerDeadBodyId; } app_fatal("Invalid TrackerTargetCategory"); } std::string_view TrackerTargetCategoryLabel(TrackerTargetCategory category) { switch (category) { case TrackerTargetCategory::Items: return _("items"); case TrackerTargetCategory::Chests: return _("chests"); case TrackerTargetCategory::Doors: return _("doors"); case TrackerTargetCategory::Shrines: return _("shrines"); case TrackerTargetCategory::Objects: return _("objects"); case TrackerTargetCategory::Breakables: return _("breakables"); case TrackerTargetCategory::Monsters: return _("monsters"); case TrackerTargetCategory::DeadBodies: return _("dead bodies"); default: return _("items"); } } void SpeakTrackerTargetCategory() { std::string message; StrAppend(message, _("Tracker target: "), TrackerTargetCategoryLabel(SelectedTrackerTargetCategory)); SpeakText(message, true); } [[nodiscard]] constexpr int CorpseTrackerIdForPosition(Point position) { return position.x + position.y * MAXDUNX; } [[nodiscard]] constexpr Point CorpsePositionForTrackerId(int corpseId) { return { corpseId % MAXDUNX, corpseId / MAXDUNX }; } 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; } [[nodiscard]] std::optional FindNearestGroundItemId(Point playerPosition) { std::optional bestId; int bestDistance = 0; for (int y = 0; y < MAXDUNY; ++y) { for (int x = 0; x < MAXDUNX; ++x) { const int itemId = std::abs(dItem[x][y]) - 1; if (itemId < 0 || itemId > MAXITEMS) continue; const Item &item = Items[itemId]; if (item.isEmpty() || item._iClass == ICLASS_NONE) continue; const int distance = playerPosition.WalkingDistance(Point { x, y }); if (!bestId || distance < bestDistance) { bestId = itemId; bestDistance = distance; } } } return bestId; } [[nodiscard]] std::optional FindNearestCorpseId(Point playerPosition) { std::optional bestId; int bestDistance = 0; for (int y = 0; y < MAXDUNY; ++y) { for (int x = 0; x < MAXDUNX; ++x) { if (dCorpse[x][y] == 0) continue; const Point position { x, y }; const int distance = playerPosition.WalkingDistance(position); if (!bestId || distance < bestDistance) { bestId = CorpseTrackerIdForPosition(position); bestDistance = distance; } } } return bestId; } struct TrackerCandidate { int id; int distance; StringOrView name; }; [[nodiscard]] bool IsBetterTrackerCandidate(const TrackerCandidate &a, const TrackerCandidate &b) { if (a.distance != b.distance) return a.distance < b.distance; return a.id < b.id; } [[nodiscard]] std::vector CollectNearbyItemTrackerCandidates(Point playerPosition, int maxDistance) { std::vector result; result.reserve(ActiveItemCount); const int minX = std::max(0, playerPosition.x - maxDistance); const int minY = std::max(0, playerPosition.y - maxDistance); const int maxX = std::min(MAXDUNX - 1, playerPosition.x + maxDistance); const int maxY = std::min(MAXDUNY - 1, playerPosition.y + maxDistance); std::array seen {}; for (int y = minY; y <= maxY; ++y) { for (int x = minX; x <= maxX; ++x) { const int itemId = std::abs(dItem[x][y]) - 1; if (itemId < 0 || itemId > MAXITEMS) continue; if (seen[itemId]) continue; seen[itemId] = true; const Item &item = Items[itemId]; if (item.isEmpty() || item._iClass == ICLASS_NONE) continue; const int distance = playerPosition.WalkingDistance(Point { x, y }); if (distance > maxDistance) continue; result.push_back(TrackerCandidate { .id = itemId, .distance = distance, .name = item.getName(), }); } } std::sort(result.begin(), result.end(), [](const TrackerCandidate &a, const TrackerCandidate &b) { return IsBetterTrackerCandidate(a, b); }); return result; } [[nodiscard]] std::vector CollectNearbyCorpseTrackerCandidates(Point playerPosition, int maxDistance) { std::vector result; const int minX = std::max(0, playerPosition.x - maxDistance); const int minY = std::max(0, playerPosition.y - maxDistance); const int maxX = std::min(MAXDUNX - 1, playerPosition.x + maxDistance); const int maxY = std::min(MAXDUNY - 1, playerPosition.y + maxDistance); for (int y = minY; y <= maxY; ++y) { for (int x = minX; x <= maxX; ++x) { if (dCorpse[x][y] == 0) continue; const Point position { x, y }; const int distance = playerPosition.WalkingDistance(position); if (distance > maxDistance) continue; result.push_back(TrackerCandidate { .id = CorpseTrackerIdForPosition(position), .distance = distance, .name = _("Dead body"), }); } } std::sort(result.begin(), result.end(), [](const TrackerCandidate &a, const TrackerCandidate &b) { return IsBetterTrackerCandidate(a, b); }); return result; } [[nodiscard]] constexpr bool IsTrackedChestObject(const Object &object) { return object.canInteractWith() && (object.IsChest() || object._otype == _object_id::OBJ_SIGNCHEST); } [[nodiscard]] constexpr bool IsTrackedDoorObject(const Object &object) { // Track both closed and open doors (to match proximity audio cues). return object.isDoor() && object.canInteractWith(); } [[nodiscard]] constexpr bool IsShrineLikeObject(const Object &object) { return object.canInteractWith() && (object.IsShrine() || IsAnyOf(object._otype, _object_id::OBJ_BLOODFTN, _object_id::OBJ_PURIFYINGFTN, _object_id::OBJ_GOATSHRINE, _object_id::OBJ_CAULDRON, _object_id::OBJ_MURKYFTN, _object_id::OBJ_TEARFTN)); } [[nodiscard]] constexpr bool IsTrackedBreakableObject(const Object &object) { return object.IsBreakable(); } [[nodiscard]] constexpr bool IsTrackedMiscInteractableObject(const Object &object) { if (!object.canInteractWith()) return false; if (object.IsChest() || object._otype == _object_id::OBJ_SIGNCHEST) return false; if (object.isDoor()) return false; if (IsShrineLikeObject(object)) return false; if (object.IsBreakable()) return false; return true; } [[nodiscard]] bool IsTrackedMonster(const Monster &monster) { return !monster.isInvalid && (monster.flags & MFLAG_HIDDEN) == 0 && monster.hitPoints > 0; } template [[nodiscard]] std::vector CollectNearbyObjectTrackerCandidates(Point playerPosition, int maxDistance, Predicate predicate) { std::vector result; result.reserve(ActiveObjectCount); const int minX = std::max(0, playerPosition.x - maxDistance); const int minY = std::max(0, playerPosition.y - maxDistance); const int maxX = std::min(MAXDUNX - 1, playerPosition.x + maxDistance); const int maxY = std::min(MAXDUNY - 1, playerPosition.y + maxDistance); std::array bestDistanceById {}; bestDistanceById.fill(std::numeric_limits::max()); for (int y = minY; y <= maxY; ++y) { for (int x = minX; x <= maxX; ++x) { const int objectId = std::abs(dObject[x][y]) - 1; if (objectId < 0 || objectId >= MAXOBJECTS) continue; const Object &object = Objects[objectId]; if (object._otype == OBJ_NULL) continue; if (!predicate(object)) continue; const int distance = playerPosition.WalkingDistance(Point { x, y }); if (distance > maxDistance) continue; int &bestDistance = bestDistanceById[objectId]; if (distance < bestDistance) bestDistance = distance; } } for (int objectId = 0; objectId < MAXOBJECTS; ++objectId) { const int distance = bestDistanceById[objectId]; if (distance == std::numeric_limits::max()) continue; const Object &object = Objects[objectId]; result.push_back(TrackerCandidate { .id = objectId, .distance = distance, .name = object.name(), }); } std::sort(result.begin(), result.end(), [](const TrackerCandidate &a, const TrackerCandidate &b) { return IsBetterTrackerCandidate(a, b); }); return result; } template [[nodiscard]] std::optional FindNearestObjectId(Point playerPosition, Predicate predicate) { std::array bestDistanceById {}; bestDistanceById.fill(std::numeric_limits::max()); for (int y = 0; y < MAXDUNY; ++y) { for (int x = 0; x < MAXDUNX; ++x) { const int objectId = std::abs(dObject[x][y]) - 1; if (objectId < 0 || objectId >= MAXOBJECTS) continue; const Object &object = Objects[objectId]; if (object._otype == OBJ_NULL) continue; if (!predicate(object)) continue; const int distance = playerPosition.WalkingDistance(Point { x, y }); int &bestDistance = bestDistanceById[objectId]; if (distance < bestDistance) bestDistance = distance; } } std::optional bestId; int bestDistance = 0; for (int objectId = 0; objectId < MAXOBJECTS; ++objectId) { const int distance = bestDistanceById[objectId]; if (distance == std::numeric_limits::max()) continue; if (!bestId || distance < bestDistance) { bestId = objectId; bestDistance = distance; } } return bestId; } [[nodiscard]] std::vector CollectNearbyChestTrackerCandidates(Point playerPosition, int maxDistance) { return CollectNearbyObjectTrackerCandidates(playerPosition, maxDistance, IsTrackedChestObject); } [[nodiscard]] std::vector CollectNearbyDoorTrackerCandidates(Point playerPosition, int maxDistance) { return CollectNearbyObjectTrackerCandidates(playerPosition, maxDistance, IsTrackedDoorObject); } [[nodiscard]] std::vector CollectNearbyShrineTrackerCandidates(Point playerPosition, int maxDistance) { return CollectNearbyObjectTrackerCandidates(playerPosition, maxDistance, IsShrineLikeObject); } [[nodiscard]] std::vector CollectNearbyBreakableTrackerCandidates(Point playerPosition, int maxDistance) { return CollectNearbyObjectTrackerCandidates(playerPosition, maxDistance, IsTrackedBreakableObject); } [[nodiscard]] std::vector CollectNearbyObjectInteractableTrackerCandidates(Point playerPosition, int maxDistance) { return CollectNearbyObjectTrackerCandidates(playerPosition, maxDistance, IsTrackedMiscInteractableObject); } [[nodiscard]] std::vector CollectNearbyMonsterTrackerCandidates(Point playerPosition, int maxDistance) { std::vector result; result.reserve(ActiveMonsterCount); for (size_t i = 0; i < ActiveMonsterCount; ++i) { const int monsterId = static_cast(ActiveMonsters[i]); const Monster &monster = Monsters[monsterId]; if (monster.isInvalid) continue; if ((monster.flags & MFLAG_HIDDEN) != 0) continue; if (monster.hitPoints <= 0) continue; const Point monsterDistancePosition { monster.position.future }; const int distance = playerPosition.ApproxDistance(monsterDistancePosition); if (distance > maxDistance) continue; result.push_back(TrackerCandidate { .id = monsterId, .distance = distance, .name = monster.name(), }); } std::sort(result.begin(), result.end(), [](const TrackerCandidate &a, const TrackerCandidate &b) { return IsBetterTrackerCandidate(a, b); }); return result; } [[nodiscard]] std::optional FindNextTrackerCandidateId(const std::vector &candidates, int currentId) { if (candidates.empty()) return std::nullopt; if (currentId < 0) return candidates.front().id; const auto it = std::find_if(candidates.begin(), candidates.end(), [currentId](const TrackerCandidate &c) { return c.id == currentId; }); if (it == candidates.end()) return candidates.front().id; if (candidates.size() <= 1) return std::nullopt; const size_t idx = static_cast(it - candidates.begin()); const size_t nextIdx = (idx + 1) % candidates.size(); return candidates[nextIdx].id; } void DecorateTrackerTargetNameWithOrdinalIfNeeded(int targetId, StringOrView &targetName, const std::vector &candidates) { if (targetName.empty()) return; const std::string_view baseName = targetName.str(); int total = 0; for (const TrackerCandidate &c : candidates) { if (c.name.str() == baseName) ++total; } if (total <= 1) return; int ordinal = 0; int seen = 0; for (const TrackerCandidate &c : candidates) { if (c.name.str() != baseName) continue; ++seen; if (c.id == targetId) { ordinal = seen; break; } } if (ordinal <= 0) return; std::string decorated; StrAppend(decorated, baseName, " ", ordinal); targetName = std::move(decorated); } [[nodiscard]] bool IsGroundItemPresent(int itemId) { if (itemId < 0 || itemId > MAXITEMS) return false; for (uint8_t i = 0; i < ActiveItemCount; ++i) { if (ActiveItems[i] == itemId) return true; } return false; } [[nodiscard]] bool IsCorpsePresent(int corpseId) { if (corpseId < 0 || corpseId >= MAXDUNX * MAXDUNY) return false; const Point position = CorpsePositionForTrackerId(corpseId); return InDungeonBounds(position) && dCorpse[position.x][position.y] != 0; } [[nodiscard]] std::optional FindNearestUnopenedChestObjectId(Point playerPosition) { return FindNearestObjectId(playerPosition, IsTrackedChestObject); } [[nodiscard]] std::optional FindNearestDoorObjectId(Point playerPosition) { return FindNearestObjectId(playerPosition, IsTrackedDoorObject); } [[nodiscard]] std::optional FindNearestShrineObjectId(Point playerPosition) { return FindNearestObjectId(playerPosition, IsShrineLikeObject); } [[nodiscard]] std::optional FindNearestBreakableObjectId(Point playerPosition) { return FindNearestObjectId(playerPosition, IsTrackedBreakableObject); } [[nodiscard]] std::optional FindNearestMiscInteractableObjectId(Point playerPosition) { return FindNearestObjectId(playerPosition, IsTrackedMiscInteractableObject); } [[nodiscard]] std::optional FindNearestMonsterId(Point playerPosition) { std::optional bestId; int bestDistance = 0; for (size_t i = 0; i < ActiveMonsterCount; ++i) { const int monsterId = static_cast(ActiveMonsters[i]); const Monster &monster = Monsters[monsterId]; if (monster.isInvalid) continue; if ((monster.flags & MFLAG_HIDDEN) != 0) continue; if (monster.hitPoints <= 0) continue; const Point monsterDistancePosition { monster.position.future }; const int distance = playerPosition.ApproxDistance(monsterDistancePosition); if (!bestId || distance < bestDistance) { bestId = monsterId; bestDistance = distance; } } return bestId; } [[nodiscard]] std::optional FindBestAdjacentApproachTile(const Player &player, Point playerPosition, Point targetPosition) { std::optional best; size_t bestPathLength = 0; int bestDistance = 0; std::optional bestFallback; int bestFallbackDistance = 0; for (int dy = -1; dy <= 1; ++dy) { for (int dx = -1; dx <= 1; ++dx) { if (dx == 0 && dy == 0) continue; const Point tile { targetPosition.x + dx, targetPosition.y + dy }; if (!PosOkPlayer(player, tile)) continue; const int distance = playerPosition.WalkingDistance(tile); if (!bestFallback || distance < bestFallbackDistance) { bestFallback = tile; bestFallbackDistance = distance; } const std::optional> path = FindKeyboardWalkPathForSpeech(player, playerPosition, tile); if (!path) continue; const size_t pathLength = path->size(); if (!best || pathLength < bestPathLength || (pathLength == bestPathLength && distance < bestDistance)) { best = tile; bestPathLength = pathLength; bestDistance = distance; } } } if (best) return best; return bestFallback; } } // namespace 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; } namespace { [[nodiscard]] 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); } } // namespace 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 { struct DoorBlockInfo { Point beforeDoor; Point doorPosition; }; std::optional FindFirstClosedDoorOnWalkPath(Point startPosition, const int8_t *path, int steps) { Point position = startPosition; for (int i = 0; i < steps; ++i) { const Point next = NextPositionForWalkDirection(position, path[i]); Object *object = FindObjectAtPosition(next); if (object != nullptr && object->isDoor() && object->_oSolidFlag) { return DoorBlockInfo { .beforeDoor = position, .doorPosition = next }; } position = next; } return std::nullopt; } enum class TrackerPathBlockType : uint8_t { Door, Monster, Breakable, }; struct TrackerPathBlockInfo { TrackerPathBlockType type; size_t stepIndex; Point beforeBlock; Point blockPosition; }; [[nodiscard]] std::optional FindFirstTrackerPathBlock(Point startPosition, const int8_t *path, size_t steps, bool considerDoors, bool considerMonsters, bool considerBreakables, Point targetPosition) { Point position = startPosition; for (size_t i = 0; i < steps; ++i) { const Point next = NextPositionForWalkDirection(position, path[i]); if (next == targetPosition) { position = next; continue; } Object *object = FindObjectAtPosition(next); if (considerDoors && object != nullptr && object->isDoor() && object->_oSolidFlag) { return TrackerPathBlockInfo { .type = TrackerPathBlockType::Door, .stepIndex = i, .beforeBlock = position, .blockPosition = next, }; } if (considerBreakables && object != nullptr && object->_oSolidFlag && object->IsBreakable()) { return TrackerPathBlockInfo { .type = TrackerPathBlockType::Breakable, .stepIndex = i, .beforeBlock = position, .blockPosition = next, }; } if (considerMonsters && leveltype != DTYPE_TOWN && dMonster[next.x][next.y] != 0) { const int monsterRef = dMonster[next.x][next.y]; const int monsterId = std::abs(monsterRef) - 1; const bool blocks = monsterRef <= 0 || (monsterId >= 0 && monsterId < static_cast(MaxMonsters) && !Monsters[monsterId].hasNoLife()); if (blocks) { return TrackerPathBlockInfo { .type = TrackerPathBlockType::Monster, .stepIndex = i, .beforeBlock = position, .blockPosition = next, }; } } position = next; } return std::nullopt; } static void NavigateToTrackerTargetKeyPressedImpl() { if (!CanPlayerTakeAction() || InGameMenu()) return; if (leveltype == DTYPE_TOWN && IsNoneOf(SelectedTrackerTargetCategory, TrackerTargetCategory::Items, TrackerTargetCategory::DeadBodies)) { SpeakText(_("Not in a dungeon."), true); return; } if (AutomapActive) { SpeakText(_("Close the map first."), true); return; } if (MyPlayer == nullptr) return; EnsureTrackerLocksMatchCurrentLevel(); const SDL_Keymod modState = SDL_GetModState(); const bool cycleTarget = (modState & SDL_KMOD_SHIFT) != 0; const bool clearTarget = (modState & SDL_KMOD_CTRL) != 0; const Point playerPosition = MyPlayer->position.future; AutoWalkTrackerTargetId = -1; int &lockedTargetId = LockedTrackerTargetId(SelectedTrackerTargetCategory); if (clearTarget) { lockedTargetId = -1; SpeakText(_("Tracker target cleared."), true); return; } std::optional targetId; std::optional targetPosition; std::optional alternateTargetPosition; StringOrView targetName; switch (SelectedTrackerTargetCategory) { case TrackerTargetCategory::Items: { const std::vector nearbyCandidates = CollectNearbyItemTrackerCandidates(playerPosition, TrackerCycleDistanceTiles); if (cycleTarget) { targetId = FindNextTrackerCandidateId(nearbyCandidates, lockedTargetId); if (!targetId) { if (nearbyCandidates.empty()) SpeakText(_("No items found."), true); else SpeakText(_("No next item."), true); return; } } else if (IsGroundItemPresent(lockedTargetId)) { targetId = lockedTargetId; } else { targetId = FindNearestGroundItemId(playerPosition); } if (!targetId) { SpeakText(_("No items found."), true); return; } if (!IsGroundItemPresent(*targetId)) { lockedTargetId = -1; SpeakText(_("No items found."), true); return; } lockedTargetId = *targetId; const Item &tracked = Items[*targetId]; targetName = tracked.getName(); DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates); targetPosition = tracked.position; break; } case TrackerTargetCategory::Chests: { const std::vector nearbyCandidates = CollectNearbyChestTrackerCandidates(playerPosition, TrackerCycleDistanceTiles); if (cycleTarget) { targetId = FindNextTrackerCandidateId(nearbyCandidates, lockedTargetId); if (!targetId) { if (nearbyCandidates.empty()) SpeakText(_("No chests found."), true); else SpeakText(_("No next chest."), true); return; } } else if (lockedTargetId >= 0 && lockedTargetId < MAXOBJECTS) { targetId = lockedTargetId; } else { targetId = FindNearestUnopenedChestObjectId(playerPosition); } if (!targetId) { SpeakText(_("No chests found."), true); return; } const Object &object = Objects[*targetId]; if (!IsTrackedChestObject(object)) { lockedTargetId = -1; targetId = FindNearestUnopenedChestObjectId(playerPosition); if (!targetId) { SpeakText(_("No chests found."), true); return; } } lockedTargetId = *targetId; const Object &tracked = Objects[*targetId]; targetName = tracked.name(); DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates); if (!cycleTarget) { targetPosition = tracked.position; if (FindObjectAtPosition(tracked.position + Direction::NorthEast) == &tracked) alternateTargetPosition = tracked.position + Direction::NorthEast; } break; } case TrackerTargetCategory::Doors: { std::vector nearbyCandidates = CollectNearbyDoorTrackerCandidates(playerPosition, TrackerCycleDistanceTiles); for (TrackerCandidate &c : nearbyCandidates) { if (c.id < 0 || c.id >= MAXOBJECTS) continue; c.name = DoorLabelForSpeech(Objects[c.id]); } if (cycleTarget) { targetId = FindNextTrackerCandidateId(nearbyCandidates, lockedTargetId); if (!targetId) { if (nearbyCandidates.empty()) SpeakText(_("No doors found."), true); else SpeakText(_("No next door."), true); return; } } else if (lockedTargetId >= 0 && lockedTargetId < MAXOBJECTS) { targetId = lockedTargetId; } else { targetId = FindNearestDoorObjectId(playerPosition); } if (!targetId) { SpeakText(_("No doors found."), true); return; } const Object &object = Objects[*targetId]; if (!IsTrackedDoorObject(object)) { lockedTargetId = -1; targetId = FindNearestDoorObjectId(playerPosition); if (!targetId) { SpeakText(_("No doors found."), true); return; } } lockedTargetId = *targetId; const Object &tracked = Objects[*targetId]; targetName = DoorLabelForSpeech(tracked); DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates); if (!cycleTarget) { targetPosition = tracked.position; if (FindObjectAtPosition(tracked.position + Direction::NorthEast) == &tracked) alternateTargetPosition = tracked.position + Direction::NorthEast; } break; } case TrackerTargetCategory::Shrines: { const std::vector nearbyCandidates = CollectNearbyShrineTrackerCandidates(playerPosition, TrackerCycleDistanceTiles); if (cycleTarget) { targetId = FindNextTrackerCandidateId(nearbyCandidates, lockedTargetId); if (!targetId) { if (nearbyCandidates.empty()) SpeakText(_("No shrines found."), true); else SpeakText(_("No next shrine."), true); return; } } else if (lockedTargetId >= 0 && lockedTargetId < MAXOBJECTS) { targetId = lockedTargetId; } else { targetId = FindNearestShrineObjectId(playerPosition); } if (!targetId) { SpeakText(_("No shrines found."), true); return; } const Object &object = Objects[*targetId]; if (!IsShrineLikeObject(object)) { lockedTargetId = -1; targetId = FindNearestShrineObjectId(playerPosition); if (!targetId) { SpeakText(_("No shrines found."), true); return; } } lockedTargetId = *targetId; const Object &tracked = Objects[*targetId]; targetName = tracked.name(); DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates); if (!cycleTarget) { targetPosition = tracked.position; if (FindObjectAtPosition(tracked.position + Direction::NorthEast) == &tracked) alternateTargetPosition = tracked.position + Direction::NorthEast; } break; } case TrackerTargetCategory::Objects: { const std::vector nearbyCandidates = CollectNearbyObjectInteractableTrackerCandidates(playerPosition, TrackerCycleDistanceTiles); if (cycleTarget) { targetId = FindNextTrackerCandidateId(nearbyCandidates, lockedTargetId); if (!targetId) { if (nearbyCandidates.empty()) SpeakText(_("No objects found."), true); else SpeakText(_("No next object."), true); return; } } else if (lockedTargetId >= 0 && lockedTargetId < MAXOBJECTS) { targetId = lockedTargetId; } else { targetId = FindNearestMiscInteractableObjectId(playerPosition); } if (!targetId) { SpeakText(_("No objects found."), true); return; } const Object &object = Objects[*targetId]; if (!IsTrackedMiscInteractableObject(object)) { lockedTargetId = -1; targetId = FindNearestMiscInteractableObjectId(playerPosition); if (!targetId) { SpeakText(_("No objects found."), true); return; } } lockedTargetId = *targetId; const Object &tracked = Objects[*targetId]; targetName = tracked.name(); DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates); if (!cycleTarget) { targetPosition = tracked.position; if (FindObjectAtPosition(tracked.position + Direction::NorthEast) == &tracked) alternateTargetPosition = tracked.position + Direction::NorthEast; } break; } case TrackerTargetCategory::Breakables: { const std::vector nearbyCandidates = CollectNearbyBreakableTrackerCandidates(playerPosition, TrackerCycleDistanceTiles); if (cycleTarget) { targetId = FindNextTrackerCandidateId(nearbyCandidates, lockedTargetId); if (!targetId) { if (nearbyCandidates.empty()) SpeakText(_("No breakables found."), true); else SpeakText(_("No next breakable."), true); return; } } else if (lockedTargetId >= 0 && lockedTargetId < MAXOBJECTS) { targetId = lockedTargetId; } else { targetId = FindNearestBreakableObjectId(playerPosition); } if (!targetId) { SpeakText(_("No breakables found."), true); return; } const Object &object = Objects[*targetId]; if (!IsTrackedBreakableObject(object)) { lockedTargetId = -1; targetId = FindNearestBreakableObjectId(playerPosition); if (!targetId) { SpeakText(_("No breakables found."), true); return; } } lockedTargetId = *targetId; const Object &tracked = Objects[*targetId]; targetName = tracked.name(); DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates); if (!cycleTarget) { targetPosition = tracked.position; if (FindObjectAtPosition(tracked.position + Direction::NorthEast) == &tracked) alternateTargetPosition = tracked.position + Direction::NorthEast; } break; } case TrackerTargetCategory::Monsters: { const std::vector nearbyCandidates = CollectNearbyMonsterTrackerCandidates(playerPosition, TrackerCycleDistanceTiles); if (cycleTarget) { targetId = FindNextTrackerCandidateId(nearbyCandidates, lockedTargetId); if (!targetId) { if (nearbyCandidates.empty()) SpeakText(_("No monsters found."), true); else SpeakText(_("No next monster."), true); return; } } else if (lockedTargetId >= 0 && lockedTargetId < static_cast(MaxMonsters)) { targetId = lockedTargetId; } else { targetId = FindNearestMonsterId(playerPosition); } if (!targetId) { SpeakText(_("No monsters found."), true); return; } const Monster &monster = Monsters[*targetId]; if (!IsTrackedMonster(monster)) { lockedTargetId = -1; targetId = FindNearestMonsterId(playerPosition); if (!targetId) { SpeakText(_("No monsters found."), true); return; } } lockedTargetId = *targetId; const Monster &tracked = Monsters[*targetId]; targetName = tracked.name(); DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates); if (!cycleTarget) { targetPosition = tracked.position.tile; } break; } case TrackerTargetCategory::DeadBodies: { const std::vector nearbyCandidates = CollectNearbyCorpseTrackerCandidates(playerPosition, TrackerCycleDistanceTiles); if (cycleTarget) { targetId = FindNextTrackerCandidateId(nearbyCandidates, lockedTargetId); if (!targetId) { if (nearbyCandidates.empty()) SpeakText(_("No dead bodies found."), true); else SpeakText(_("No next dead body."), true); return; } } else if (IsCorpsePresent(lockedTargetId)) { targetId = lockedTargetId; } else { targetId = FindNearestCorpseId(playerPosition); } if (!targetId) { SpeakText(_("No dead bodies found."), true); return; } if (!IsCorpsePresent(*targetId)) { lockedTargetId = -1; SpeakText(_("No dead bodies found."), true); return; } lockedTargetId = *targetId; targetName = _("Dead body"); DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates); if (!cycleTarget) { targetPosition = CorpsePositionForTrackerId(*targetId); } break; } } if (cycleTarget) { SpeakText(targetName.str(), /*force=*/true); return; } if (!targetPosition) { SpeakText(_("Can't find a nearby tile to walk to."), true); return; } Point chosenTargetPosition = *targetPosition; enum class TrackerPathMode : uint8_t { RespectDoorsAndMonsters, IgnoreDoors, IgnoreMonsters, IgnoreDoorsAndMonsters, Lenient, }; auto findPathToTarget = [&](Point destination, TrackerPathMode mode) -> std::optional> { const bool allowDestinationNonWalkable = !PosOkPlayer(*MyPlayer, destination); switch (mode) { case TrackerPathMode::RespectDoorsAndMonsters: return FindKeyboardWalkPathForSpeechRespectingDoors(*MyPlayer, playerPosition, destination, allowDestinationNonWalkable); case TrackerPathMode::IgnoreDoors: return FindKeyboardWalkPathForSpeech(*MyPlayer, playerPosition, destination, allowDestinationNonWalkable); case TrackerPathMode::IgnoreMonsters: return FindKeyboardWalkPathForSpeechRespectingDoorsIgnoringMonsters(*MyPlayer, playerPosition, destination, allowDestinationNonWalkable); case TrackerPathMode::IgnoreDoorsAndMonsters: return FindKeyboardWalkPathForSpeechIgnoringMonsters(*MyPlayer, playerPosition, destination, allowDestinationNonWalkable); case TrackerPathMode::Lenient: return FindKeyboardWalkPathForSpeechLenient(*MyPlayer, playerPosition, destination, allowDestinationNonWalkable); default: return std::nullopt; } }; std::optional> spokenPath; bool pathIgnoresDoors = false; bool pathIgnoresMonsters = false; bool pathIgnoresBreakables = false; const auto considerDestination = [&](Point destination, TrackerPathMode mode) { const std::optional> candidate = findPathToTarget(destination, mode); if (!candidate) return; if (!spokenPath || candidate->size() < spokenPath->size()) { spokenPath = *candidate; chosenTargetPosition = destination; pathIgnoresDoors = mode == TrackerPathMode::IgnoreDoors || mode == TrackerPathMode::IgnoreDoorsAndMonsters || mode == TrackerPathMode::Lenient; pathIgnoresMonsters = mode == TrackerPathMode::IgnoreMonsters || mode == TrackerPathMode::IgnoreDoorsAndMonsters || mode == TrackerPathMode::Lenient; pathIgnoresBreakables = mode == TrackerPathMode::Lenient; } }; considerDestination(*targetPosition, TrackerPathMode::RespectDoorsAndMonsters); if (alternateTargetPosition) considerDestination(*alternateTargetPosition, TrackerPathMode::RespectDoorsAndMonsters); if (!spokenPath) { considerDestination(*targetPosition, TrackerPathMode::IgnoreDoors); if (alternateTargetPosition) considerDestination(*alternateTargetPosition, TrackerPathMode::IgnoreDoors); } if (!spokenPath) { considerDestination(*targetPosition, TrackerPathMode::IgnoreMonsters); if (alternateTargetPosition) considerDestination(*alternateTargetPosition, TrackerPathMode::IgnoreMonsters); } if (!spokenPath) { considerDestination(*targetPosition, TrackerPathMode::IgnoreDoorsAndMonsters); if (alternateTargetPosition) considerDestination(*alternateTargetPosition, TrackerPathMode::IgnoreDoorsAndMonsters); } if (!spokenPath) { considerDestination(*targetPosition, TrackerPathMode::Lenient); if (alternateTargetPosition) considerDestination(*alternateTargetPosition, TrackerPathMode::Lenient); } bool showUnreachableWarning = false; if (!spokenPath) { showUnreachableWarning = true; Point closestPosition; spokenPath = FindKeyboardWalkPathToClosestReachableForSpeech(*MyPlayer, playerPosition, chosenTargetPosition, closestPosition); pathIgnoresDoors = true; pathIgnoresMonsters = false; pathIgnoresBreakables = false; } if (spokenPath && !showUnreachableWarning && !PosOkPlayer(*MyPlayer, chosenTargetPosition)) { if (!spokenPath->empty()) spokenPath->pop_back(); } if (spokenPath && (pathIgnoresDoors || pathIgnoresMonsters || pathIgnoresBreakables)) { const std::optional block = FindFirstTrackerPathBlock(playerPosition, spokenPath->data(), spokenPath->size(), pathIgnoresDoors, pathIgnoresMonsters, pathIgnoresBreakables, chosenTargetPosition); if (block) { if (playerPosition.WalkingDistance(block->blockPosition) <= TrackerInteractDistanceTiles) { switch (block->type) { case TrackerPathBlockType::Door: SpeakText(_("A door is blocking the path. Open it and try again."), true); return; case TrackerPathBlockType::Monster: SpeakText(_("A monster is blocking the path. Clear it and try again."), true); return; case TrackerPathBlockType::Breakable: SpeakText(_("A breakable object is blocking the path. Destroy it and try again."), true); return; } } spokenPath = std::vector(spokenPath->begin(), spokenPath->begin() + block->stepIndex); } } std::string message; if (!targetName.empty()) StrAppend(message, targetName, "\n"); if (showUnreachableWarning) { message.append(_("Can't find a path to the target.")); if (spokenPath && !spokenPath->empty()) message.append("\n"); } if (spokenPath) { if (!showUnreachableWarning || !spokenPath->empty()) AppendKeyboardWalkPathForSpeech(message, *spokenPath); } SpeakText(message, true); } } // namespace void NavigateToTrackerTargetKeyPressed() { NavigateToTrackerTargetKeyPressedImpl(); } namespace { /** * Validates an object-category auto-walk target and computes the walk destination. */ template bool ValidateAutoWalkObjectTarget( const Player &myPlayer, Point playerPosition, Predicate isValid, const char *goneMessage, const char *inRangeMessage, std::optional &destination) { const int objectId = AutoWalkTrackerTargetId; if (objectId < 0 || objectId >= MAXOBJECTS) { AutoWalkTrackerTargetId = -1; SpeakText(_(goneMessage), true); return false; } const Object &object = Objects[objectId]; if (!isValid(object)) { AutoWalkTrackerTargetId = -1; SpeakText(_(goneMessage), true); return false; } if (playerPosition.WalkingDistance(object.position) <= TrackerInteractDistanceTiles) { AutoWalkTrackerTargetId = -1; SpeakText(_(inRangeMessage), true); return false; } destination = FindBestAdjacentApproachTile(myPlayer, playerPosition, object.position); return true; } /** * Resolves which object to walk toward for the given tracker category. */ template std::optional ResolveObjectTrackerTarget( int &lockedTargetId, Point playerPosition, Predicate isValid, FindNearest findNearest, GetName getName, const char *notFoundMessage, StringOrView &targetName) { std::optional targetId; if (lockedTargetId >= 0 && lockedTargetId < MAXOBJECTS) { targetId = lockedTargetId; } else { targetId = findNearest(playerPosition); } if (!targetId) { SpeakText(_(notFoundMessage), true); return std::nullopt; } if (!isValid(Objects[*targetId])) { lockedTargetId = -1; targetId = findNearest(playerPosition); if (!targetId) { SpeakText(_(notFoundMessage), true); return std::nullopt; } // findNearest guarantees the result passes isValid, but verify defensively. if (!isValid(Objects[*targetId])) { SpeakText(_(notFoundMessage), true); return std::nullopt; } } lockedTargetId = *targetId; targetName = getName(*targetId); return targetId; } } // namespace void CycleTrackerTargetKeyPressed() { if (!CanPlayerTakeAction() || InGameMenu()) return; AutoWalkTrackerTargetId = -1; const SDL_Keymod modState = SDL_GetModState(); const bool cyclePrevious = (modState & SDL_KMOD_SHIFT) != 0; if (cyclePrevious) { switch (SelectedTrackerTargetCategory) { case TrackerTargetCategory::Items: SelectedTrackerTargetCategory = TrackerTargetCategory::DeadBodies; break; case TrackerTargetCategory::Chests: SelectedTrackerTargetCategory = TrackerTargetCategory::Items; break; case TrackerTargetCategory::Doors: SelectedTrackerTargetCategory = TrackerTargetCategory::Chests; break; case TrackerTargetCategory::Shrines: SelectedTrackerTargetCategory = TrackerTargetCategory::Doors; break; case TrackerTargetCategory::Objects: SelectedTrackerTargetCategory = TrackerTargetCategory::Shrines; break; case TrackerTargetCategory::Breakables: SelectedTrackerTargetCategory = TrackerTargetCategory::Objects; break; case TrackerTargetCategory::Monsters: SelectedTrackerTargetCategory = TrackerTargetCategory::Breakables; break; case TrackerTargetCategory::DeadBodies: default: SelectedTrackerTargetCategory = TrackerTargetCategory::Monsters; break; } } else { switch (SelectedTrackerTargetCategory) { case TrackerTargetCategory::Items: SelectedTrackerTargetCategory = TrackerTargetCategory::Chests; break; case TrackerTargetCategory::Chests: SelectedTrackerTargetCategory = TrackerTargetCategory::Doors; break; case TrackerTargetCategory::Doors: SelectedTrackerTargetCategory = TrackerTargetCategory::Shrines; break; case TrackerTargetCategory::Shrines: SelectedTrackerTargetCategory = TrackerTargetCategory::Objects; break; case TrackerTargetCategory::Objects: SelectedTrackerTargetCategory = TrackerTargetCategory::Breakables; break; case TrackerTargetCategory::Breakables: SelectedTrackerTargetCategory = TrackerTargetCategory::Monsters; break; case TrackerTargetCategory::Monsters: SelectedTrackerTargetCategory = TrackerTargetCategory::DeadBodies; break; case TrackerTargetCategory::DeadBodies: default: SelectedTrackerTargetCategory = TrackerTargetCategory::Items; break; } } SpeakTrackerTargetCategory(); } /** * Called each game tick to advance auto-walk toward the current tracker target. */ void UpdateAutoWalkTracker() { if (AutoWalkTrackerTargetId < 0) return; if (leveltype == DTYPE_TOWN || IsPlayerInStore() || ChatLogFlag || HelpFlag || InGameMenu()) { AutoWalkTrackerTargetId = -1; return; } if (!CanPlayerTakeAction()) return; if (MyPlayer == nullptr) { SpeakText(_("Cannot walk right now."), true); return; } if (MyPlayer->_pmode != PM_STAND) return; if (MyPlayer->walkpath[0] != WALK_NONE) return; if (MyPlayer->destAction != ACTION_NONE) return; Player &myPlayer = *MyPlayer; const Point playerPosition = myPlayer.position.future; std::optional destination; switch (AutoWalkTrackerTargetCategory) { case TrackerTargetCategory::Items: { const int itemId = AutoWalkTrackerTargetId; if (itemId < 0 || itemId > MAXITEMS) { AutoWalkTrackerTargetId = -1; SpeakText(_("Target item is gone."), true); return; } if (!IsGroundItemPresent(itemId)) { AutoWalkTrackerTargetId = -1; SpeakText(_("Target item is gone."), true); return; } const Item &item = Items[itemId]; if (playerPosition.WalkingDistance(item.position) <= TrackerInteractDistanceTiles) { AutoWalkTrackerTargetId = -1; SpeakText(_("Item in range."), true); return; } destination = item.position; break; } case TrackerTargetCategory::Chests: if (!ValidateAutoWalkObjectTarget(myPlayer, playerPosition, IsTrackedChestObject, N_("Target chest is gone."), N_("Chest in range."), destination)) return; break; case TrackerTargetCategory::Doors: if (!ValidateAutoWalkObjectTarget(myPlayer, playerPosition, IsTrackedDoorObject, N_("Target door is gone."), N_("Door in range."), destination)) return; break; case TrackerTargetCategory::Shrines: if (!ValidateAutoWalkObjectTarget(myPlayer, playerPosition, IsShrineLikeObject, N_("Target shrine is gone."), N_("Shrine in range."), destination)) return; break; case TrackerTargetCategory::Objects: if (!ValidateAutoWalkObjectTarget(myPlayer, playerPosition, IsTrackedMiscInteractableObject, N_("Target object is gone."), N_("Object in range."), destination)) return; break; case TrackerTargetCategory::Breakables: if (!ValidateAutoWalkObjectTarget(myPlayer, playerPosition, IsTrackedBreakableObject, N_("Target breakable is gone."), N_("Breakable in range."), destination)) return; break; case TrackerTargetCategory::Monsters: { const int monsterId = AutoWalkTrackerTargetId; if (monsterId < 0 || monsterId >= static_cast(MaxMonsters)) { AutoWalkTrackerTargetId = -1; SpeakText(_("Target monster is gone."), true); return; } const Monster &monster = Monsters[monsterId]; if (!IsTrackedMonster(monster)) { AutoWalkTrackerTargetId = -1; SpeakText(_("Target monster is gone."), true); return; } const Point monsterPosition { monster.position.tile }; if (playerPosition.WalkingDistance(monsterPosition) <= TrackerInteractDistanceTiles) { AutoWalkTrackerTargetId = -1; SpeakText(_("Monster in range."), true); return; } destination = FindBestAdjacentApproachTile(myPlayer, playerPosition, monsterPosition); break; } case TrackerTargetCategory::DeadBodies: { const int corpseId = AutoWalkTrackerTargetId; if (!IsCorpsePresent(corpseId)) { AutoWalkTrackerTargetId = -1; SpeakText(_("Target dead body is gone."), true); return; } const Point corpsePosition = CorpsePositionForTrackerId(corpseId); if (playerPosition.WalkingDistance(corpsePosition) <= TrackerInteractDistanceTiles) { AutoWalkTrackerTargetId = -1; SpeakText(_("Dead body in range."), true); return; } destination = corpsePosition; break; } } if (!destination) { AutoWalkTrackerTargetId = -1; SpeakText(_("Can't find a nearby tile to walk to."), true); return; } constexpr size_t MaxAutoWalkPathLength = 512; std::array path; path.fill(WALK_NONE); int steps = FindPath(CanStep, [&myPlayer](Point position) { return PosOkPlayer(myPlayer, position); }, playerPosition, *destination, path.data(), path.size()); // If no direct path exists, try pathfinding that treats closed doors as walkable. // If that finds a path, identify the first closed door along it and re-route the // player to the tile just before that door, so they can open it and retry. if (steps == 0) { std::array ignoreDoorPath; ignoreDoorPath.fill(WALK_NONE); const int ignoreDoorSteps = FindPath(CanStep, [&myPlayer](Point position) { return PosOkPlayerIgnoreDoors(myPlayer, position); }, playerPosition, *destination, ignoreDoorPath.data(), ignoreDoorPath.size()); if (ignoreDoorSteps != 0) { const std::optional block = FindFirstClosedDoorOnWalkPath(playerPosition, ignoreDoorPath.data(), ignoreDoorSteps); if (block) { if (playerPosition.WalkingDistance(block->doorPosition) <= TrackerInteractDistanceTiles) { AutoWalkTrackerTargetId = -1; SpeakText(_("A door is blocking the path. Open it and try again."), true); return; } *destination = block->beforeDoor; path.fill(WALK_NONE); steps = FindPath(CanStep, [&myPlayer](Point position) { return PosOkPlayer(myPlayer, position); }, playerPosition, *destination, path.data(), path.size()); } } if (steps == 0) { AutoWalkTrackerTargetId = -1; SpeakText(_("Can't find a path to the target."), 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(MaxPathLengthPlayer)) { NetSendCmdLoc(MyPlayerId, true, CMD_WALKXY, *destination); return; } const int segmentSteps = std::min(steps - 1, static_cast(MaxPathLengthPlayer - 1)); const Point waypoint = PositionAfterWalkPathSteps(playerPosition, path.data(), segmentSteps); NetSendCmdLoc(MyPlayerId, true, CMD_WALKXY, waypoint); } /** * Initiates auto-walk toward the currently selected tracker target. */ void AutoWalkToTrackerTargetKeyPressed() { // Cancel in-progress auto-walk (must be checked before the action guard // so that cancellation works even if the player is mid-walk). if (AutoWalkTrackerTargetId >= 0) { CancelAutoWalk(); SpeakText(_("Walk cancelled."), true); return; } // Defense-in-depth: keymapper canTrigger also checks these, but guard here // in case the function is called from another code path. if (!CanPlayerTakeAction() || InGameMenu()) return; if (leveltype == DTYPE_TOWN) { SpeakText(_("Not in a dungeon."), true); return; } if (AutomapActive) { SpeakText(_("Close the map first."), true); return; } if (MyPlayer == nullptr) { SpeakText(_("Cannot walk right now."), true); return; } EnsureTrackerLocksMatchCurrentLevel(); const Point playerPosition = MyPlayer->position.future; int &lockedTargetId = LockedTrackerTargetId(SelectedTrackerTargetCategory); std::optional targetId; StringOrView targetName; switch (SelectedTrackerTargetCategory) { case TrackerTargetCategory::Items: { if (IsGroundItemPresent(lockedTargetId)) { targetId = lockedTargetId; } else { targetId = FindNearestGroundItemId(playerPosition); } if (!targetId) { SpeakText(_("No items found."), true); return; } if (!IsGroundItemPresent(*targetId)) { lockedTargetId = -1; SpeakText(_("No items found."), true); return; } lockedTargetId = *targetId; targetName = Items[*targetId].getName(); break; } case TrackerTargetCategory::Chests: targetId = ResolveObjectTrackerTarget(lockedTargetId, playerPosition, IsTrackedChestObject, FindNearestUnopenedChestObjectId, [](int id) -> StringOrView { return Objects[id].name(); }, N_("No chests found."), targetName); if (!targetId) return; break; case TrackerTargetCategory::Doors: targetId = ResolveObjectTrackerTarget(lockedTargetId, playerPosition, IsTrackedDoorObject, FindNearestDoorObjectId, [](int id) -> StringOrView { return DoorLabelForSpeech(Objects[id]); }, N_("No doors found."), targetName); if (!targetId) return; break; case TrackerTargetCategory::Shrines: targetId = ResolveObjectTrackerTarget(lockedTargetId, playerPosition, IsShrineLikeObject, FindNearestShrineObjectId, [](int id) -> StringOrView { return Objects[id].name(); }, N_("No shrines found."), targetName); if (!targetId) return; break; case TrackerTargetCategory::Objects: targetId = ResolveObjectTrackerTarget(lockedTargetId, playerPosition, IsTrackedMiscInteractableObject, FindNearestMiscInteractableObjectId, [](int id) -> StringOrView { return Objects[id].name(); }, N_("No objects found."), targetName); if (!targetId) return; break; case TrackerTargetCategory::Breakables: targetId = ResolveObjectTrackerTarget(lockedTargetId, playerPosition, IsTrackedBreakableObject, FindNearestBreakableObjectId, [](int id) -> StringOrView { return Objects[id].name(); }, N_("No breakables found."), targetName); if (!targetId) return; break; case TrackerTargetCategory::Monsters: { if (lockedTargetId >= 0 && lockedTargetId < static_cast(MaxMonsters)) { targetId = lockedTargetId; } else { targetId = FindNearestMonsterId(playerPosition); } if (!targetId) { SpeakText(_("No monsters found."), true); return; } const Monster &monster = Monsters[*targetId]; if (!IsTrackedMonster(monster)) { lockedTargetId = -1; targetId = FindNearestMonsterId(playerPosition); if (!targetId) { SpeakText(_("No monsters found."), true); return; } } lockedTargetId = *targetId; targetName = Monsters[*targetId].name(); break; } case TrackerTargetCategory::DeadBodies: { if (IsCorpsePresent(lockedTargetId)) { targetId = lockedTargetId; } else { targetId = FindNearestCorpseId(playerPosition); } if (!targetId) { SpeakText(_("No dead bodies found."), true); return; } if (!IsCorpsePresent(*targetId)) { lockedTargetId = -1; SpeakText(_("No dead bodies found."), true); return; } lockedTargetId = *targetId; targetName = _("Dead body"); break; } } std::string msg; StrAppend(msg, _("Going to: "), targetName); SpeakText(msg, true); AutoWalkTrackerTargetId = *targetId; AutoWalkTrackerTargetCategory = SelectedTrackerTargetCategory; UpdateAutoWalkTracker(); } } // namespace devilution