/** * @file utils/walk_path_speech.cpp * * Walk-path helpers, PosOk variants, and BFS pathfinding for accessibility speech. */ #include "utils/walk_path_speech.hpp" #include #include #include #include #include #include #include #include #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 std::optional> FindKeyboardWalkPathForSpeechBfs(const Player &player, Point startPosition, Point destinationPosition, PosOkForSpeechFn posOk, const std::array &walkDirections, bool allowDiagonalSteps, bool allowDestinationNonWalkable) { if (!InDungeonBounds(startPosition) || !InDungeonBounds(destinationPosition)) return std::nullopt; if (startPosition == destinationPosition) return std::vector {}; std::array visited {}; std::array parentDir {}; parentDir.fill(WALK_NONE); std::queue queue; const auto indexOf = [](Point position) -> size_t { return static_cast(position.x) + static_cast(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 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 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> FindKeyboardWalkPathForSpeechWithPosOk(const Player &player, Point startPosition, Point destinationPosition, PosOkForSpeechFn posOk, bool allowDestinationNonWalkable) { constexpr std::array AxisDirections = { WALK_NE, WALK_SW, WALK_SE, WALK_NW, }; constexpr std::array AllDirections = { WALK_NE, WALK_SW, WALK_SE, WALK_NW, WALK_N, WALK_E, WALK_S, WALK_W, }; if (const std::optional> axisPath = FindKeyboardWalkPathForSpeechBfs(player, startPosition, destinationPosition, posOk, AxisDirections, /*allowDiagonalSteps=*/false, allowDestinationNonWalkable); axisPath) { return axisPath; } return FindKeyboardWalkPathForSpeechBfs(player, startPosition, destinationPosition, posOk, AllDirections, /*allowDiagonalSteps=*/true, allowDestinationNonWalkable); } template std::optional> FindKeyboardWalkPathToClosestReachableForSpeechBfs(const Player &player, Point startPosition, Point destinationPosition, PosOkForSpeechFn posOk, const std::array &walkDirections, bool allowDiagonalSteps, Point &closestPosition) { if (!InDungeonBounds(startPosition) || !InDungeonBounds(destinationPosition)) return std::nullopt; if (startPosition == destinationPosition) { closestPosition = destinationPosition; return std::vector {}; } std::array visited {}; std::array parentDir {}; std::array depth {}; parentDir.fill(WALK_NONE); depth.fill(0); std::queue queue; const auto indexOf = [](Point position) -> size_t { return static_cast(position.x) + static_cast(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(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 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 {}; std::vector 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> FindKeyboardWalkPathForSpeech(const Player &player, Point startPosition, Point destinationPosition, bool allowDestinationNonWalkable) { return FindKeyboardWalkPathForSpeechWithPosOk(player, startPosition, destinationPosition, PosOkPlayerIgnoreDoors, allowDestinationNonWalkable); } std::optional> FindKeyboardWalkPathForSpeechRespectingDoors(const Player &player, Point startPosition, Point destinationPosition, bool allowDestinationNonWalkable) { return FindKeyboardWalkPathForSpeechWithPosOk(player, startPosition, destinationPosition, PosOkPlayer, allowDestinationNonWalkable); } std::optional> FindKeyboardWalkPathForSpeechIgnoringMonsters(const Player &player, Point startPosition, Point destinationPosition, bool allowDestinationNonWalkable) { return FindKeyboardWalkPathForSpeechWithPosOk(player, startPosition, destinationPosition, PosOkPlayerIgnoreDoorsAndMonsters, allowDestinationNonWalkable); } std::optional> FindKeyboardWalkPathForSpeechRespectingDoorsIgnoringMonsters(const Player &player, Point startPosition, Point destinationPosition, bool allowDestinationNonWalkable) { return FindKeyboardWalkPathForSpeechWithPosOk(player, startPosition, destinationPosition, PosOkPlayerIgnoreMonsters, allowDestinationNonWalkable); } std::optional> FindKeyboardWalkPathForSpeechLenient(const Player &player, Point startPosition, Point destinationPosition, bool allowDestinationNonWalkable) { return FindKeyboardWalkPathForSpeechWithPosOk(player, startPosition, destinationPosition, PosOkPlayerIgnoreDoorsMonstersAndBreakables, allowDestinationNonWalkable); } std::optional> FindKeyboardWalkPathToClosestReachableForSpeech(const Player &player, Point startPosition, Point destinationPosition, Point &closestPosition) { constexpr std::array AxisDirections = { WALK_NE, WALK_SW, WALK_SE, WALK_NW, }; constexpr std::array AllDirections = { WALK_NE, WALK_SW, WALK_SE, WALK_NW, WALK_N, WALK_E, WALK_S, WALK_W, }; Point axisClosest; const std::optional> axisPath = FindKeyboardWalkPathToClosestReachableForSpeechBfs(player, startPosition, destinationPosition, PosOkPlayerIgnoreDoors, AxisDirections, /*allowDiagonalSteps=*/false, axisClosest); Point diagClosest; const std::optional> 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 &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