From 4fbe18fe2b58532bc555979904698aad6d347141 Mon Sep 17 00:00:00 2001 From: mojsior Date: Sun, 25 Jan 2026 21:51:37 +0100 Subject: [PATCH] Expand tracker categories for dungeon navigation Adds new tracker categories (T) for doors, shrines, interactable objects, and breakables, and updates N/Shift+N to navigate/cycle within these categories. Object tracking uses the dungeon object grid (dObject) for reliable discovery, and Polish translations were updated. --- Source/diablo.cpp | 423 +++++++++++++++++++++++++++++++++++++++++---- Translations/pl.po | 52 +++++- 2 files changed, 439 insertions(+), 36 deletions(-) diff --git a/Source/diablo.cpp b/Source/diablo.cpp index a21c1d710..6a987a80c 100644 --- a/Source/diablo.cpp +++ b/Source/diablo.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -1881,6 +1882,10 @@ int AutoWalkTownNpcTarget = -1; enum class TrackerTargetCategory : uint8_t { Items, Chests, + Doors, + Shrines, + Objects, + Breakables, Monsters, }; @@ -2168,6 +2173,10 @@ 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; struct TrackerLevelKey { @@ -2183,6 +2192,10 @@ void ClearTrackerLocks() { LockedTrackerItemId = -1; LockedTrackerChestId = -1; + LockedTrackerDoorId = -1; + LockedTrackerShrineId = -1; + LockedTrackerObjectId = -1; + LockedTrackerBreakableId = -1; LockedTrackerMonsterId = -1; } @@ -2209,6 +2222,14 @@ int &LockedTrackerTargetId(TrackerTargetCategory category) 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: default: return LockedTrackerMonsterId; @@ -2222,6 +2243,14 @@ std::string_view TrackerTargetCategoryLabel(TrackerTargetCategory category) 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"); default: @@ -2248,6 +2277,18 @@ void CycleTrackerTargetKeyPressed() 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: @@ -2339,24 +2380,87 @@ struct TrackerCandidate { return result; } -[[nodiscard]] std::vector CollectNearbyChestTrackerCandidates(Point playerPosition, int maxDistance) +[[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) +{ + // Only closed doors (solid), because open doors are mostly just floor. + return object.isDoor() && object.canInteractWith() && object._oSolidFlag; +} + +[[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; +} + +template +[[nodiscard]] std::vector CollectNearbyObjectTrackerCandidates(Point playerPosition, int maxDistance, Predicate predicate) { std::vector result; result.reserve(ActiveObjectCount); - for (int i = 0; i < ActiveObjectCount; i++) { - const int objectId = ActiveObjects[i]; - if (objectId < 0 || objectId >= MAXOBJECTS) - continue; + 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); - const Object &object = Objects[objectId]; - if (!object.canInteractWith() || !object.IsChest()) - continue; + std::array bestDistanceById {}; + bestDistanceById.fill(std::numeric_limits::max()); - const int distance = playerPosition.WalkingDistance(object.position); - if (distance > maxDistance) + 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, @@ -2368,6 +2472,72 @@ struct TrackerCandidate { 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; @@ -2481,31 +2651,27 @@ void DecorateTrackerTargetNameWithOrdinalIfNeeded(int targetId, StringOrView &ta std::optional FindNearestUnopenedChestObjectId(Point playerPosition) { - if (ActiveObjectCount == 0) - return std::nullopt; - - std::optional bestId; - int bestDistance = 0; + return FindNearestObjectId(playerPosition, IsTrackedChestObject); +} - for (int i = 0; i < ActiveObjectCount; i++) { - const int objectId = ActiveObjects[i]; - if (objectId < 0 || objectId >= MAXOBJECTS) - continue; +std::optional FindNearestClosedDoorObjectId(Point playerPosition) +{ + return FindNearestObjectId(playerPosition, IsTrackedDoorObject); +} - const Object &object = Objects[objectId]; - if (!object.canInteractWith()) - continue; - if (!object.IsChest()) - continue; +std::optional FindNearestShrineObjectId(Point playerPosition) +{ + return FindNearestObjectId(playerPosition, IsShrineLikeObject); +} - const int distance = playerPosition.WalkingDistance(object.position); - if (!bestId || distance < bestDistance) { - bestId = objectId; - bestDistance = distance; - } - } +std::optional FindNearestBreakableObjectId(Point playerPosition) +{ + return FindNearestObjectId(playerPosition, IsTrackedBreakableObject); +} - return bestId; +std::optional FindNearestMiscInteractableObjectId(Point playerPosition) +{ + return FindNearestObjectId(playerPosition, IsTrackedMiscInteractableObject); } std::optional FindNearestMonsterId(Point playerPosition) @@ -2569,6 +2735,15 @@ std::optional FindBestAdjacentApproachTile(const Player &player, Point pl 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; + + return FindBestAdjacentApproachTile(player, playerPosition, object.position); +} + bool PosOkPlayerIgnoreDoors(const Player &player, Point position) { if (!InDungeonBounds(position)) @@ -2704,7 +2879,7 @@ void NavigateToTrackerTargetKeyPressed() } const Object &object = Objects[*targetId]; - if (!object.IsChest() || !object.canInteractWith()) { + if (!IsTrackedChestObject(object)) { lockedTargetId = -1; targetId = FindNearestUnopenedChestObjectId(playerPosition); if (!targetId) { @@ -2719,7 +2894,187 @@ void NavigateToTrackerTargetKeyPressed() targetName = tracked.name(); DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates); if (!cycleTarget) { - targetPosition = FindBestAdjacentApproachTile(*MyPlayer, playerPosition, tracked.position); + targetPosition = FindBestApproachTileForObject(*MyPlayer, playerPosition, tracked); + if (!targetPosition) { + SpeakText(_("Can't find a nearby tile to walk to."), true); + return; + } + } + break; + } + case TrackerTargetCategory::Doors: { + const std::vector nearbyCandidates = CollectNearbyDoorTrackerCandidates(playerPosition, TrackerCycleDistanceTiles); + 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 = FindNearestClosedDoorObjectId(playerPosition); + } + if (!targetId) { + SpeakText(_("No doors found."), true); + return; + } + + const Object &object = Objects[*targetId]; + if (!IsTrackedDoorObject(object)) { + lockedTargetId = -1; + targetId = FindNearestClosedDoorObjectId(playerPosition); + if (!targetId) { + SpeakText(_("No doors found."), true); + return; + } + } + + lockedTargetId = *targetId; + const Object &tracked = Objects[*targetId]; + + 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; + } + } + 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 = FindBestApproachTileForObject(*MyPlayer, playerPosition, tracked); + if (!targetPosition) { + SpeakText(_("Can't find a nearby tile to walk to."), true); + return; + } + } + 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 = FindBestApproachTileForObject(*MyPlayer, playerPosition, tracked); + if (!targetPosition) { + SpeakText(_("Can't find a nearby tile to walk to."), true); + return; + } + } + 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 = FindBestApproachTileForObject(*MyPlayer, playerPosition, tracked); if (!targetPosition) { SpeakText(_("Can't find a nearby tile to walk to."), true); return; @@ -4106,7 +4461,7 @@ void InitKeymapActions() options.Keymapper.AddAction( "CycleTrackerTarget", N_("Cycle tracker target"), - N_("Cycles what the tracker looks for (items, chests, monsters)."), + N_("Cycles what the tracker looks for (items, chests, doors, shrines, objects, breakables, monsters)."), 'T', CycleTrackerTargetKeyPressed, nullptr, diff --git a/Translations/pl.po b/Translations/pl.po index 927c93ffa..1c702fe5b 100644 --- a/Translations/pl.po +++ b/Translations/pl.po @@ -6508,6 +6508,22 @@ msgstr "Nie ma następnego przedmiotu." msgid "No next chest." msgstr "Nie ma następnej skrzyni." +#: Source/diablo.cpp +msgid "No next door." +msgstr "Nie ma następnych drzwi." + +#: Source/diablo.cpp +msgid "No next shrine." +msgstr "Nie ma następnej kapliczki." + +#: Source/diablo.cpp +msgid "No next object." +msgstr "Nie ma następnego obiektu." + +#: Source/diablo.cpp +msgid "No next breakable." +msgstr "Nie ma następnego niszczalnego obiektu." + #: Source/diablo.cpp msgid "No next monster." msgstr "Nie ma następnego potwora." @@ -12147,8 +12163,8 @@ msgid "Cycle tracker target" msgstr "Zmień cel trackera" #: Source/diablo.cpp -msgid "Cycles what the tracker looks for (items, chests, monsters)." -msgstr "Zmienia, czego szuka tracker (przedmioty, skrzynie, potwory)." +msgid "Cycles what the tracker looks for (items, chests, doors, shrines, objects, breakables, monsters)." +msgstr "Zmienia, czego szuka tracker (przedmioty, skrzynie, drzwi, kapliczki, obiekty, niszczalne, potwory)." #: Source/diablo.cpp msgid "Navigate to tracker target" @@ -12170,6 +12186,22 @@ msgstr "przedmioty" msgid "chests" msgstr "skrzynie" +#: Source/diablo.cpp +msgid "doors" +msgstr "drzwi" + +#: Source/diablo.cpp +msgid "shrines" +msgstr "kapliczki" + +#: Source/diablo.cpp +msgid "objects" +msgstr "obiekty" + +#: Source/diablo.cpp +msgid "breakables" +msgstr "niszczalne" + #: Source/diablo.cpp msgid "monsters" msgstr "potwory" @@ -12182,6 +12214,22 @@ msgstr "Nie znaleziono żadnych przedmiotów." msgid "No chests found." msgstr "Nie znaleziono żadnych skrzyń." +#: Source/diablo.cpp +msgid "No doors found." +msgstr "Nie znaleziono żadnych drzwi." + +#: Source/diablo.cpp +msgid "No shrines found." +msgstr "Nie znaleziono żadnych kapliczek." + +#: Source/diablo.cpp +msgid "No objects found." +msgstr "Nie znaleziono żadnych obiektów." + +#: Source/diablo.cpp +msgid "No breakables found." +msgstr "Nie znaleziono żadnych niszczalnych obiektów." + #: Source/diablo.cpp msgid "No monsters found." msgstr "Nie znaleziono żadnych potworów."