From e56b7d96d08b2b495e604f1ffcee71e29eab23e5 Mon Sep 17 00:00:00 2001 From: mojsior Date: Mon, 26 Jan 2026 11:50:01 +0100 Subject: [PATCH] Fix N tracker pathfinding for non-items --- Source/diablo.cpp | 776 +++++++++++++++++++++++++++++++++++++-------- Translations/pl.po | 24 ++ 2 files changed, 661 insertions(+), 139 deletions(-) diff --git a/Source/diablo.cpp b/Source/diablo.cpp index 2041eb7cd..5a54a03ea 100644 --- a/Source/diablo.cpp +++ b/Source/diablo.cpp @@ -181,8 +181,13 @@ char gszVersionNumber[64] = "internal version unknown"; void UpdateAutoWalkTracker(); void SpeakSelectedSpeedbookSpell(); void SpellBookKeyPressed(); - std::optional> FindKeyboardWalkPathForSpeech(const Player &player, Point startPosition, Point destinationPosition); - void AppendKeyboardWalkPathForSpeech(std::string &message, const std::vector &path); +std::optional> FindKeyboardWalkPathForSpeech(const Player &player, Point startPosition, Point destinationPosition, bool allowDestinationNonWalkable = false); +std::optional> FindKeyboardWalkPathForSpeechRespectingDoors(const Player &player, Point startPosition, Point destinationPosition, bool allowDestinationNonWalkable = false); +std::optional> FindKeyboardWalkPathForSpeechIgnoringMonsters(const Player &player, Point startPosition, Point destinationPosition, bool allowDestinationNonWalkable = false); +std::optional> FindKeyboardWalkPathForSpeechRespectingDoorsIgnoringMonsters(const Player &player, Point startPosition, Point destinationPosition, bool allowDestinationNonWalkable = false); +std::optional> FindKeyboardWalkPathForSpeechLenient(const Player &player, Point startPosition, Point destinationPosition, bool allowDestinationNonWalkable = false); +std::optional> FindKeyboardWalkPathToClosestReachableForSpeech(const Player &player, Point startPosition, Point destinationPosition, Point &closestPosition); +void AppendKeyboardWalkPathForSpeech(std::string &message, const std::vector &path); void AppendDirectionalFallback(std::string &message, const Displacement &delta); bool gbGameLoopStartup; @@ -2539,8 +2544,8 @@ struct TrackerCandidate { [[nodiscard]] constexpr bool IsTrackedDoorObject(const Object &object) { - // Only closed doors (solid), because open doors are mostly just floor. - return object.isDoor() && object.canInteractWith() && object._oSolidFlag; + // Track both closed and open doors (to match proximity audio cues). + return object.isDoor() && object.canInteractWith(); } [[nodiscard]] constexpr bool IsShrineLikeObject(const Object &object) @@ -2695,41 +2700,27 @@ template std::vector result; result.reserve(ActiveMonsterCount); - 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 monsterId = std::abs(dMonster[x][y]) - 1; - if (monsterId < 0 || monsterId >= static_cast(MaxMonsters)) - continue; - if (seen[monsterId]) - continue; - seen[monsterId] = true; + for (size_t i = 0; i < ActiveMonsterCount; ++i) { + const int monsterId = static_cast(ActiveMonsters[i]); + const Monster &monster = Monsters[monsterId]; - const Monster &monster = Monsters[monsterId]; - if (monster.isInvalid) - continue; - if ((monster.flags & MFLAG_HIDDEN) != 0) - continue; - if (monster.hitPoints <= 0) - continue; + if (monster.isInvalid) + continue; + if ((monster.flags & MFLAG_HIDDEN) != 0) + continue; + if (monster.hitPoints <= 0) + continue; - const Point monsterPosition { monster.position.tile }; - const int distance = playerPosition.WalkingDistance(monsterPosition); - if (distance > maxDistance) - 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(), - }); - } + 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); }); @@ -2806,7 +2797,7 @@ std::optional FindNearestUnopenedChestObjectId(Point playerPosition) return FindNearestObjectId(playerPosition, IsTrackedChestObject); } -std::optional FindNearestClosedDoorObjectId(Point playerPosition) +std::optional FindNearestDoorObjectId(Point playerPosition) { return FindNearestObjectId(playerPosition, IsTrackedDoorObject); } @@ -2831,31 +2822,22 @@ std::optional FindNearestMonsterId(Point playerPosition) std::optional bestId; int bestDistance = 0; - std::array seen {}; - - for (int y = 0; y < MAXDUNY; ++y) { - for (int x = 0; x < MAXDUNX; ++x) { - const int monsterId = std::abs(dMonster[x][y]) - 1; - if (monsterId < 0 || monsterId >= static_cast(MaxMonsters)) - continue; - if (seen[monsterId]) - continue; - seen[monsterId] = true; + for (size_t i = 0; i < ActiveMonsterCount; ++i) { + const int monsterId = static_cast(ActiveMonsters[i]); + const Monster &monster = Monsters[monsterId]; - const Monster &monster = Monsters[monsterId]; - if (monster.isInvalid) - continue; - if ((monster.flags & MFLAG_HIDDEN) != 0) - continue; - if (monster.hitPoints <= 0) - continue; + if (monster.isInvalid) + continue; + if ((monster.flags & MFLAG_HIDDEN) != 0) + continue; + if (monster.hitPoints <= 0) + continue; - const Point monsterPosition { monster.position.tile }; - const int distance = playerPosition.WalkingDistance(monsterPosition); - if (!bestId || distance < bestDistance) { - bestId = monsterId; - bestDistance = distance; - } + const Point monsterDistancePosition { monster.position.future }; + const int distance = playerPosition.ApproxDistance(monsterDistancePosition); + if (!bestId || distance < bestDistance) { + bestId = monsterId; + bestDistance = distance; } } @@ -2865,8 +2847,12 @@ std::optional FindNearestMonsterId(Point playerPosition) 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) @@ -2877,23 +2863,29 @@ std::optional FindBestAdjacentApproachTile(const Player &player, Point pl continue; const int distance = playerPosition.WalkingDistance(tile); - if (!best || distance < bestDistance) { + + 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; } } } - return best; -} - -std::optional FindBestApproachTileForObject(const Player &player, Point playerPosition, const Object &object) -{ - // Some interactable objects are placed on a walkable tile (e.g. floor switches). Prefer stepping on the tile in that case. - if (!object._oSolidFlag && PosOkPlayer(player, object.position)) - return object.position; + if (best) + return best; - return FindBestAdjacentApproachTile(player, playerPosition, object.position); + return bestFallback; } bool PosOkPlayerIgnoreDoors(const Player &player, Point position) @@ -2919,6 +2911,122 @@ bool PosOkPlayerIgnoreDoors(const Player &player, Point position) return true; } +[[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); +} + +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; +} + +std::optional FindBestApproachTileForObject(const Player &player, Point playerPosition, const Object &object) +{ + // Some interactable objects are placed on a walkable tile (e.g. floor switches). Prefer stepping on the tile in that case. + if (!object._oSolidFlag && PosOkPlayer(player, object.position)) + return object.position; + + std::optional best; + size_t bestPathLength = 0; + int bestDistance = 0; + + std::optional bestFallback; + int bestFallbackDistance = 0; + + const auto considerTile = [&](Point tile) { + if (!PosOkPlayerIgnoreDoors(player, tile)) + return; + + const int distance = playerPosition.WalkingDistance(tile); + if (!bestFallback || distance < bestFallbackDistance) { + bestFallback = tile; + bestFallbackDistance = distance; + } + + const std::optional> path = FindKeyboardWalkPathForSpeech(player, playerPosition, tile); + if (!path) + return; + + const size_t pathLength = path->size(); + if (!best || pathLength < bestPathLength || (pathLength == bestPathLength && distance < bestDistance)) { + best = tile; + bestPathLength = pathLength; + bestDistance = distance; + } + }; + + for (int dy = -1; dy <= 1; ++dy) { + for (int dx = -1; dx <= 1; ++dx) { + if (dx == 0 && dy == 0) + continue; + considerTile(object.position + Displacement { dx, dy }); + } + } + + if (FindObjectAtPosition(object.position + Direction::NorthEast) == &object) { + // Special case for large objects (e.g. sarcophagi): allow approaching from one tile further to the north. + for (int dx = -1; dx <= 1; ++dx) { + considerTile(object.position + Displacement { dx, -2 }); + } + } + + if (best) + return best; + + return bestFallback; +} + struct DoorBlockInfo { Point beforeDoor; Point doorPosition; @@ -2938,6 +3046,67 @@ std::optional FindFirstClosedDoorOnWalkPath(Point startPosition, 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; +} + void NavigateToTrackerTargetKeyPressed() { if (!CanPlayerTakeAction() || InGameMenu()) @@ -2971,6 +3140,7 @@ void NavigateToTrackerTargetKeyPressed() std::optional targetId; std::optional targetPosition; + std::optional alternateTargetPosition; StringOrView targetName; switch (SelectedTrackerTargetCategory) { @@ -3046,11 +3216,9 @@ void NavigateToTrackerTargetKeyPressed() targetName = tracked.name(); DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates); if (!cycleTarget) { - targetPosition = FindBestApproachTileForObject(*MyPlayer, playerPosition, tracked); - if (!targetPosition) { - SpeakText(_("Can't find a nearby tile to walk to."), true); - return; - } + targetPosition = tracked.position; + if (FindObjectAtPosition(tracked.position + Direction::NorthEast) == &tracked) + alternateTargetPosition = tracked.position + Direction::NorthEast; } break; } @@ -3068,7 +3236,7 @@ void NavigateToTrackerTargetKeyPressed() } else if (lockedTargetId >= 0 && lockedTargetId < MAXOBJECTS) { targetId = lockedTargetId; } else { - targetId = FindNearestClosedDoorObjectId(playerPosition); + targetId = FindNearestDoorObjectId(playerPosition); } if (!targetId) { SpeakText(_("No doors found."), true); @@ -3078,7 +3246,7 @@ void NavigateToTrackerTargetKeyPressed() const Object &object = Objects[*targetId]; if (!IsTrackedDoorObject(object)) { lockedTargetId = -1; - targetId = FindNearestClosedDoorObjectId(playerPosition); + targetId = FindNearestDoorObjectId(playerPosition); if (!targetId) { SpeakText(_("No doors found."), true); return; @@ -3091,11 +3259,9 @@ void NavigateToTrackerTargetKeyPressed() targetName = tracked.name(); DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates); if (!cycleTarget) { - targetPosition = FindBestApproachTileForObject(*MyPlayer, playerPosition, tracked); - if (!targetPosition) { - SpeakText(_("Can't find a nearby tile to walk to."), true); - return; - } + targetPosition = tracked.position; + if (FindObjectAtPosition(tracked.position + Direction::NorthEast) == &tracked) + alternateTargetPosition = tracked.position + Direction::NorthEast; } break; } @@ -3136,11 +3302,9 @@ void NavigateToTrackerTargetKeyPressed() targetName = tracked.name(); DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates); if (!cycleTarget) { - targetPosition = FindBestApproachTileForObject(*MyPlayer, playerPosition, tracked); - if (!targetPosition) { - SpeakText(_("Can't find a nearby tile to walk to."), true); - return; - } + targetPosition = tracked.position; + if (FindObjectAtPosition(tracked.position + Direction::NorthEast) == &tracked) + alternateTargetPosition = tracked.position + Direction::NorthEast; } break; } @@ -3181,11 +3345,9 @@ void NavigateToTrackerTargetKeyPressed() targetName = tracked.name(); DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates); if (!cycleTarget) { - targetPosition = FindBestApproachTileForObject(*MyPlayer, playerPosition, tracked); - if (!targetPosition) { - SpeakText(_("Can't find a nearby tile to walk to."), true); - return; - } + targetPosition = tracked.position; + if (FindObjectAtPosition(tracked.position + Direction::NorthEast) == &tracked) + alternateTargetPosition = tracked.position + Direction::NorthEast; } break; } @@ -3226,11 +3388,9 @@ void NavigateToTrackerTargetKeyPressed() targetName = tracked.name(); DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates); if (!cycleTarget) { - targetPosition = FindBestApproachTileForObject(*MyPlayer, playerPosition, tracked); - if (!targetPosition) { - SpeakText(_("Can't find a nearby tile to walk to."), true); - return; - } + targetPosition = tracked.position; + if (FindObjectAtPosition(tracked.position + Direction::NorthEast) == &tracked) + alternateTargetPosition = tracked.position + Direction::NorthEast; } break; } @@ -3272,12 +3432,7 @@ void NavigateToTrackerTargetKeyPressed() targetName = tracked.name(); DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates); if (!cycleTarget) { - const Point monsterPosition { tracked.position.tile }; - targetPosition = FindBestAdjacentApproachTile(*MyPlayer, playerPosition, monsterPosition); - if (!targetPosition) { - SpeakText(_("Can't find a nearby tile to walk to."), true); - return; - } + targetPosition = tracked.position.tile; } break; } @@ -3292,14 +3447,127 @@ void NavigateToTrackerTargetKeyPressed() return; } - const std::optional> path = FindKeyboardWalkPathForSpeech(*MyPlayer, playerPosition, *targetPosition); + 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 (!path) { - AppendDirectionalFallback(message, *targetPosition - playerPosition); - } else { - AppendKeyboardWalkPathForSpeech(message, *path); + 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); @@ -3512,28 +3780,12 @@ void ListTownNpcsKeyPressed() SpeakText(output, true); } -bool IsTileNavigableForSpeech(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 (leveltype == DTYPE_TOWN) { - // In town, treat NPCs as blocking. - if (dMonster[position.x][position.y] != 0) - return false; - } +namespace { - return true; -} +using PosOkForSpeechFn = bool (*)(const Player &, Point); -std::optional> FindKeyboardWalkPathForSpeech(const Player &player, Point startPosition, Point destinationPosition) +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; @@ -3560,10 +3812,14 @@ std::optional> FindKeyboardWalkPathForSpeech(const Player &p if (visited[idx]) return; - if (!IsTileNavigableForSpeech(player, next)) - return; - if (!CanStep(current, next)) - 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; @@ -3577,22 +3833,199 @@ std::optional> FindKeyboardWalkPathForSpeech(const Player &p return visited[indexOf(destinationPosition)]; }; - constexpr std::array WalkDirections = { + 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, }; - while (!queue.empty() && !hasReachedDestination()) { + 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); +} + +} // 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); +} + +namespace { + +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; + std::array prioritizedDirs; size_t prioritizedCount = 0; const auto addUniqueDir = [&](int8_t dir) { @@ -3608,6 +4041,12 @@ std::optional> FindKeyboardWalkPathForSpeech(const Player &p 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); @@ -3615,7 +4054,7 @@ std::optional> FindKeyboardWalkPathForSpeech(const Player &p addUniqueDir(yDir); addUniqueDir(xDir); } - for (const int8_t dir : WalkDirections) { + for (const int8_t dir : walkDirections) { addUniqueDir(dir); } @@ -3624,11 +4063,12 @@ std::optional> FindKeyboardWalkPathForSpeech(const Player &p } } - if (!hasReachedDestination()) - return std::nullopt; + closestPosition = best; + if (best == startPosition) + return std::vector {}; std::vector path; - Point position = destinationPosition; + Point position = best; while (position != startPosition) { const int8_t dir = parentDir[indexOf(position)]; if (dir == WALK_NONE) @@ -3642,6 +4082,56 @@ std::optional> FindKeyboardWalkPathForSpeech(const Player &p return path; } +} // namespace + +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()) { @@ -3669,6 +4159,14 @@ void AppendKeyboardWalkPathForSpeech(std::string &message, const std::vector