You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

3024 lines
97 KiB

/**
* @file controls/tracker.cpp
*
* Tracker system for accessibility: target cycling, pathfinding, and auto-walk.
*/
#include "controls/tracker.hpp"
#include <algorithm>
#include <array>
#include <cstdint>
#include <limits>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
#include <fmt/format.h>
#ifdef USE_SDL3
#include <SDL3/SDL_keycode.h>
#else
#include <SDL.h>
#endif
#include "appfat.h"
#include "automap.h"
#include "controls/accessibility_keys.hpp"
#include "controls/plrctrls.h"
#include "diablo.h"
#include "engine/path.h"
#include "gamemenu.h"
#include "help.h"
#include "items.h"
#include "levels/gendung.h"
#include "levels/setmaps.h"
#include "levels/tile_properties.hpp"
#include "levels/trigs.h"
#include "missiles.h"
#include "monster.h"
#include "multi.h"
#include "objects.h"
#include "player.h"
#include "portal.h"
#include "qol/chatlog.h"
#include "quests.h"
#include "stores.h"
#include "towners.h"
#include "utils/accessibility_announcements.hpp"
#include "utils/is_of.hpp"
#include "utils/language.h"
#include "utils/navigation_speech.hpp"
#include "utils/screen_reader.hpp"
#include "utils/sdl_compat.h"
#include "utils/str_cat.hpp"
#include "utils/string_or_view.hpp"
#include "utils/walk_path_speech.hpp"
namespace devilution {
namespace {
TrackerTargetCategory SelectedTrackerTargetCategory = TrackerTargetCategory::Items;
TrackerTargetCategory AutoWalkTrackerTargetCategory = TrackerTargetCategory::Items; ///< Category of the active auto-walk target.
int AutoWalkTrackerTargetId = -1; ///< ID of the target being auto-walked to, or -1 if inactive.
/// Maximum Chebyshev distance (in tiles) at which the player is considered
/// close enough to interact with a tracker target.
constexpr int TrackerInteractDistanceTiles = 1;
// Selection list range for PageUp/PageDown. Use a value larger than the maximum
// possible distance across the 112x112 dungeon grid so the list includes all
// eligible targets on the current level.
constexpr int TrackerCycleDistanceTiles = MAXDUNX + MAXDUNY;
int LockedTrackerItemId = -1;
int LockedTrackerChestId = -1;
int LockedTrackerDoorId = -1;
int LockedTrackerShrineId = -1;
int LockedTrackerObjectId = -1;
int LockedTrackerBreakableId = -1;
int LockedTrackerMonsterId = -1;
int LockedTrackerDeadBodyId = -1;
int LockedTrackerNpcId = -1;
int LockedTrackerPlayerId = -1;
int LockedTrackerDungeonEntranceId = -1;
int LockedTrackerStairsId = -1;
int LockedTrackerQuestLocationId = -1;
int LockedTrackerPortalId = -1;
struct TrackerLevelKey {
dungeon_type levelType;
int currLevel;
bool isSetLevel;
int setLevelNum;
friend bool operator==(const TrackerLevelKey &lhs, const TrackerLevelKey &rhs)
{
return lhs.levelType == rhs.levelType && lhs.currLevel == rhs.currLevel
&& lhs.isSetLevel == rhs.isSetLevel && lhs.setLevelNum == rhs.setLevelNum;
}
friend bool operator!=(const TrackerLevelKey &lhs, const TrackerLevelKey &rhs)
{
return !(lhs == rhs);
}
};
std::optional<TrackerLevelKey> LockedTrackerLevelKey;
void ClearTrackerLocks()
{
LockedTrackerItemId = -1;
LockedTrackerChestId = -1;
LockedTrackerDoorId = -1;
LockedTrackerShrineId = -1;
LockedTrackerObjectId = -1;
LockedTrackerBreakableId = -1;
LockedTrackerMonsterId = -1;
LockedTrackerDeadBodyId = -1;
LockedTrackerNpcId = -1;
LockedTrackerPlayerId = -1;
LockedTrackerDungeonEntranceId = -1;
LockedTrackerStairsId = -1;
LockedTrackerQuestLocationId = -1;
LockedTrackerPortalId = -1;
}
void EnsureTrackerLocksMatchCurrentLevel()
{
const TrackerLevelKey current {
.levelType = leveltype,
.currLevel = currlevel,
.isSetLevel = setlevel,
.setLevelNum = setlvlnum,
};
if (!LockedTrackerLevelKey || *LockedTrackerLevelKey != current) {
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;
case TrackerTargetCategory::Npcs:
return LockedTrackerNpcId;
case TrackerTargetCategory::Players:
return LockedTrackerPlayerId;
case TrackerTargetCategory::DungeonEntrances:
return LockedTrackerDungeonEntranceId;
case TrackerTargetCategory::Stairs:
return LockedTrackerStairsId;
case TrackerTargetCategory::QuestLocations:
return LockedTrackerQuestLocationId;
case TrackerTargetCategory::Portals:
return LockedTrackerPortalId;
}
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");
case TrackerTargetCategory::Npcs:
return _("NPCs");
case TrackerTargetCategory::Players:
return _("players");
case TrackerTargetCategory::DungeonEntrances:
if (leveltype != DTYPE_TOWN)
return _("exits");
return _("dungeon entrances");
case TrackerTargetCategory::Stairs:
return _("stairs");
case TrackerTargetCategory::QuestLocations:
return _("quest locations");
case TrackerTargetCategory::Portals:
return _("portals");
}
app_fatal("Invalid TrackerTargetCategory");
}
void SpeakTrackerTargetCategory()
{
SpeakText(TrackerTargetCategoryLabel(SelectedTrackerTargetCategory), true);
}
std::optional<int> FindNearestGroundItemId(Point playerPosition)
{
std::optional<int> 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]] constexpr int CorpseTrackerIdForPosition(Point position)
{
return position.x + position.y * MAXDUNX;
}
[[nodiscard]] constexpr Point CorpsePositionForTrackerId(int corpseId)
{
return { corpseId % MAXDUNX, corpseId / MAXDUNX };
}
std::optional<int> FindNearestCorpseId(Point playerPosition)
{
std::optional<int> 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]] constexpr int RedPortalTrackerIdForPosition(Point position)
{
// Encode tile position into a stable negative id.
// MAXDUNX/MAXDUNY are 112, so this easily fits in int.
return -((position.y * MAXDUNX) + position.x + 1);
}
[[nodiscard]] constexpr bool IsRedPortalTrackerId(int id)
{
return id < 0;
}
[[nodiscard]] constexpr Point RedPortalPositionForTrackerId(int id)
{
const int encoded = -id - 1;
return { encoded % MAXDUNX, encoded / MAXDUNX };
}
[[nodiscard]] StringOrView ItemLabelForSpeech(const Item &item)
{
const StringOrView name = item.getName();
if (name.empty())
return name;
switch (item._iMagical) {
case ITEM_QUALITY_MAGIC:
return StrCat(name, ", ", _("magic item"));
case ITEM_QUALITY_UNIQUE:
return StrCat(name, ", ", _("unique item"));
default:
return name;
}
}
[[nodiscard]] std::vector<TrackerCandidate> CollectNearbyItemTrackerCandidates(Point playerPosition, int maxDistance)
{
std::vector<TrackerCandidate> 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<bool, MAXITEMS + 1> 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 = ItemLabelForSpeech(item),
});
}
}
std::sort(result.begin(), result.end(), IsBetterTrackerCandidate);
return result;
}
[[nodiscard]] std::vector<TrackerCandidate> CollectNearbyCorpseTrackerCandidates(Point playerPosition, int maxDistance)
{
std::vector<TrackerCandidate> 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(), IsBetterTrackerCandidate);
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)
{
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]] bool IsLazarusMagicCircleObject(const Object &object)
{
return setlevel && setlvlnum == SL_VILEBETRAYER && IsAnyOf(object._otype, _object_id::OBJ_MCIRCLE1, _object_id::OBJ_MCIRCLE2);
}
[[nodiscard]] int TrackerObjectInteractDistance(const Object &object)
{
return IsLazarusMagicCircleObject(object) ? 0 : TrackerInteractDistanceTiles;
}
[[nodiscard]] StringOrView TrackerObjectLabelForSpeech(const Object &object)
{
if (IsLazarusMagicCircleObject(object)) {
if (object._otype == _object_id::OBJ_MCIRCLE1)
return _("Central magic circle");
return _("Magic circle");
}
return object.name();
}
[[nodiscard]] bool IsTrackedMiscInteractableObject(const Object &object)
{
if (IsLazarusMagicCircleObject(object))
return true;
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
&& !(monster.type().type == MT_GOLEM && monster.position.tile == GolemHoldingCell);
}
template <typename Predicate>
[[nodiscard]] std::vector<TrackerCandidate> CollectNearbyObjectTrackerCandidates(Point playerPosition, int maxDistance, Predicate predicate)
{
std::vector<TrackerCandidate> 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<int, MAXOBJECTS> bestDistanceById {};
bestDistanceById.fill(std::numeric_limits<int>::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<int>::max())
continue;
const Object &object = Objects[objectId];
result.push_back(TrackerCandidate {
.id = objectId,
.distance = distance,
.name = TrackerObjectLabelForSpeech(object),
});
}
std::sort(result.begin(), result.end(), IsBetterTrackerCandidate);
return result;
}
template <typename Predicate>
[[nodiscard]] std::optional<int> FindNearestObjectId(Point playerPosition, Predicate predicate)
{
std::array<int, MAXOBJECTS> bestDistanceById {};
bestDistanceById.fill(std::numeric_limits<int>::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<int> bestId;
int bestDistance = 0;
for (int objectId = 0; objectId < MAXOBJECTS; ++objectId) {
const int distance = bestDistanceById[objectId];
if (distance == std::numeric_limits<int>::max())
continue;
if (!bestId || distance < bestDistance) {
bestId = objectId;
bestDistance = distance;
}
}
return bestId;
}
[[nodiscard]] std::vector<TrackerCandidate> CollectNearbyChestTrackerCandidates(Point playerPosition, int maxDistance)
{
return CollectNearbyObjectTrackerCandidates(playerPosition, maxDistance, IsTrackedChestObject);
}
[[nodiscard]] std::vector<TrackerCandidate> CollectNearbyDoorTrackerCandidates(Point playerPosition, int maxDistance)
{
return CollectNearbyObjectTrackerCandidates(playerPosition, maxDistance, IsTrackedDoorObject);
}
[[nodiscard]] std::vector<TrackerCandidate> CollectNearbyShrineTrackerCandidates(Point playerPosition, int maxDistance)
{
return CollectNearbyObjectTrackerCandidates(playerPosition, maxDistance, IsShrineLikeObject);
}
[[nodiscard]] std::vector<TrackerCandidate> CollectNearbyBreakableTrackerCandidates(Point playerPosition, int maxDistance)
{
return CollectNearbyObjectTrackerCandidates(playerPosition, maxDistance, IsTrackedBreakableObject);
}
[[nodiscard]] std::vector<TrackerCandidate> CollectNearbyObjectInteractableTrackerCandidates(Point playerPosition, int maxDistance)
{
return CollectNearbyObjectTrackerCandidates(playerPosition, maxDistance, IsTrackedMiscInteractableObject);
}
[[nodiscard]] std::vector<TrackerCandidate> CollectNearbyMonsterTrackerCandidates(Point playerPosition, int maxDistance)
{
std::vector<TrackerCandidate> result;
result.reserve(ActiveMonsterCount);
for (size_t i = 0; i < ActiveMonsterCount; ++i) {
const int monsterId = static_cast<int>(ActiveMonsters[i]);
const Monster &monster = Monsters[monsterId];
if (!IsTrackedMonster(monster))
continue;
const int distance = playerPosition.ApproxDistance(monster.position.future);
if (distance > maxDistance)
continue;
result.push_back(TrackerCandidate {
.id = monsterId,
.distance = distance,
.name = MonsterLabelForSpeech(monster),
});
}
std::sort(result.begin(), result.end(), IsBetterTrackerCandidate);
return result;
}
[[nodiscard]] std::vector<TrackerCandidate> CollectNpcTrackerCandidates(Point playerPosition)
{
std::vector<TrackerCandidate> result;
if (leveltype != DTYPE_TOWN)
return result;
result.reserve(GetNumTowners());
for (size_t i = 0; i < GetNumTowners(); ++i) {
const Towner &towner = Towners[i];
if (!IsTownerPresent(towner._ttype))
continue;
const int distance = playerPosition.WalkingDistance(towner.position);
result.push_back(TrackerCandidate {
.id = static_cast<int>(i),
.distance = distance,
.name = towner.name,
});
}
std::sort(result.begin(), result.end(), [](const TrackerCandidate &a, const TrackerCandidate &b) {
if (a.distance != b.distance)
return a.distance < b.distance;
return a.name.str() < b.name.str();
});
return result;
}
[[nodiscard]] std::vector<TrackerCandidate> CollectPlayerTrackerCandidates(Point playerPosition)
{
std::vector<TrackerCandidate> result;
if (!gbIsMultiplayer || MyPlayer == nullptr)
return result;
result.reserve(MAX_PLRS);
const uint8_t currentLevel = MyPlayer->plrlevel;
const bool currentIsSetLevel = setlevel;
for (int i = 0; i < MAX_PLRS; ++i) {
if (i == MyPlayerId)
continue;
const Player &player = Players[i];
if (!player.plractive)
continue;
if (player._pLvlChanging)
continue;
if (player.plrlevel != currentLevel)
continue;
if (player.plrIsOnSetLevel != currentIsSetLevel)
continue;
const Point otherPosition = player.position.future;
if (!InDungeonBounds(otherPosition))
continue;
const int distance = playerPosition.WalkingDistance(otherPosition);
result.push_back(TrackerCandidate {
.id = i,
.distance = distance,
.name = player.name(),
});
}
std::sort(result.begin(), result.end(), IsBetterTrackerCandidate);
return result;
}
[[nodiscard]] std::vector<TrackerCandidate> CollectDungeonEntranceTrackerCandidates(Point playerPosition)
{
std::vector<TrackerCandidate> result;
if (MyPlayer == nullptr)
return result;
if (leveltype == DTYPE_TOWN) {
const std::vector<int> candidates = CollectTownDungeonTriggerIndices();
result.reserve(candidates.size());
for (const int triggerIndex : candidates) {
if (triggerIndex < 0 || triggerIndex >= numtrigs)
continue;
const TriggerStruct &trigger = trigs[triggerIndex];
const Point triggerPosition { trigger.position.x, trigger.position.y };
const int distance = playerPosition.WalkingDistance(triggerPosition);
result.push_back(TrackerCandidate {
.id = triggerIndex,
.distance = distance,
.name = TriggerLabelForSpeech(trigger),
});
}
std::sort(result.begin(), result.end(), IsBetterTrackerCandidate);
return result;
}
for (int i = 0; i < numtrigs; ++i) {
const TriggerStruct &trigger = trigs[i];
if (setlevel) {
if (trigger._tmsg != WM_DIABRTNLVL)
continue;
} else {
if (!IsAnyOf(trigger._tmsg, WM_DIABPREVLVL, WM_DIABTWARPUP))
continue;
}
const Point triggerPosition { trigger.position.x, trigger.position.y };
const int distance = playerPosition.WalkingDistance(triggerPosition);
result.push_back(TrackerCandidate {
.id = i,
.distance = distance,
.name = TriggerLabelForSpeech(trigger),
});
}
// Lazarus' set level (SL_VILEBETRAYER) uses a RedPortal missile instead of a return trigger.
// Include it so the player can navigate out like other quest levels.
if (setlevel) {
for (const Missile &missile : Missiles) {
if (missile._mitype != MissileID::RedPortal)
continue;
const Point portalPosition = missile.position.tile;
if (!InDungeonBounds(portalPosition))
continue;
const int distance = playerPosition.WalkingDistance(portalPosition);
result.push_back(TrackerCandidate {
.id = RedPortalTrackerIdForPosition(portalPosition),
.distance = distance,
.name = _("Red portal"),
});
}
}
std::sort(result.begin(), result.end(), IsBetterTrackerCandidate);
return result;
}
[[nodiscard]] std::optional<Point> FindTownPortalPositionInTownByPortalIndex(int portalIndex)
{
if (portalIndex < 0 || portalIndex >= MAXPORTAL)
return std::nullopt;
for (const Missile &missile : Missiles) {
if (missile._mitype != MissileID::TownPortal)
continue;
if (missile._misource != portalIndex)
continue;
return missile.position.tile;
}
return std::nullopt;
}
[[nodiscard]] bool IsTownPortalOpenOnCurrentLevel(int portalIndex)
{
if (portalIndex < 0 || portalIndex >= MAXPORTAL)
return false;
const Portal &portal = Portals[portalIndex];
if (!portal.open)
return false;
if (portal.setlvl != setlevel)
return false;
if (portal.level != currlevel)
return false;
if (portal.ltype != leveltype)
return false;
return InDungeonBounds(portal.position);
}
[[nodiscard]] std::vector<TrackerCandidate> CollectPortalTrackerCandidates(Point playerPosition)
{
std::vector<TrackerCandidate> result;
if (MyPlayer == nullptr)
return result;
if (leveltype == DTYPE_TOWN) {
std::array<bool, MAXPORTAL> seen {};
for (const Missile &missile : Missiles) {
if (missile._mitype != MissileID::TownPortal)
continue;
const int portalIndex = missile._misource;
if (portalIndex < 0 || portalIndex >= MAXPORTAL)
continue;
if (seen[portalIndex])
continue;
seen[portalIndex] = true;
const Point portalPosition = missile.position.tile;
const int distance = playerPosition.WalkingDistance(portalPosition);
result.push_back(TrackerCandidate {
.id = portalIndex,
.distance = distance,
.name = TownPortalLabelForSpeech(Portals[portalIndex]),
});
}
std::sort(result.begin(), result.end(), IsBetterTrackerCandidate);
return result;
}
for (int i = 0; i < MAXPORTAL; ++i) {
if (!IsTownPortalOpenOnCurrentLevel(i))
continue;
const Portal &portal = Portals[i];
const int distance = playerPosition.WalkingDistance(portal.position);
result.push_back(TrackerCandidate {
.id = i,
.distance = distance,
.name = TownPortalLabelForSpeech(portal),
});
}
std::sort(result.begin(), result.end(), IsBetterTrackerCandidate);
return result;
}
[[nodiscard]] std::vector<TrackerCandidate> CollectStairsTrackerCandidates(Point playerPosition)
{
std::vector<TrackerCandidate> result;
if (MyPlayer == nullptr || leveltype == DTYPE_TOWN)
return result;
for (int i = 0; i < numtrigs; ++i) {
const TriggerStruct &trigger = trigs[i];
if (!IsAnyOf(trigger._tmsg, WM_DIABNEXTLVL, WM_DIABPREVLVL, WM_DIABTWARPUP))
continue;
const Point triggerPosition { trigger.position.x, trigger.position.y };
const int distance = playerPosition.WalkingDistance(triggerPosition);
result.push_back(TrackerCandidate {
.id = i,
.distance = distance,
.name = TriggerLabelForSpeech(trigger),
});
}
std::sort(result.begin(), result.end(), IsBetterTrackerCandidate);
return result;
}
[[nodiscard]] std::vector<TrackerCandidate> CollectQuestLocationTrackerCandidates(Point playerPosition)
{
std::vector<TrackerCandidate> result;
if (MyPlayer == nullptr || leveltype == DTYPE_TOWN)
return result;
if (setlevel) {
for (int i = 0; i < numtrigs; ++i) {
const TriggerStruct &trigger = trigs[i];
if (trigger._tmsg != WM_DIABRTNLVL)
continue;
const Point triggerPosition { trigger.position.x, trigger.position.y };
const int distance = playerPosition.WalkingDistance(triggerPosition);
result.push_back(TrackerCandidate {
.id = i,
.distance = distance,
.name = TriggerLabelForSpeech(trigger),
});
}
// Lazarus' set level (SL_VILEBETRAYER) uses a RedPortal missile instead of a return trigger.
for (const Missile &missile : Missiles) {
if (missile._mitype != MissileID::RedPortal)
continue;
const Point portalPosition = missile.position.tile;
if (!InDungeonBounds(portalPosition))
continue;
const int distance = playerPosition.WalkingDistance(portalPosition);
result.push_back(TrackerCandidate {
.id = RedPortalTrackerIdForPosition(portalPosition),
.distance = distance,
.name = _("Red portal"),
});
}
std::sort(result.begin(), result.end(), IsBetterTrackerCandidate);
return result;
}
constexpr size_t NumQuests = sizeof(Quests) / sizeof(Quests[0]);
result.reserve(NumQuests);
for (size_t questIndex = 0; questIndex < NumQuests; ++questIndex) {
const Quest &quest = Quests[questIndex];
if (quest._qslvl == SL_NONE)
continue;
if (quest._qactive == QUEST_NOTAVAIL)
continue;
if (quest._qlevel != currlevel)
continue;
if (!InDungeonBounds(quest.position))
continue;
const char *questLevelName = QuestLevelNames[quest._qslvl];
if (questLevelName == nullptr || questLevelName[0] == '\0')
questLevelName = N_("Set level");
const int distance = playerPosition.WalkingDistance(quest.position);
result.push_back(TrackerCandidate {
.id = static_cast<int>(questIndex),
.distance = distance,
.name = _(questLevelName),
});
}
std::sort(result.begin(), result.end(), IsBetterTrackerCandidate);
return result;
}
[[nodiscard]] std::optional<int> FindNextTrackerCandidateId(const std::vector<TrackerCandidate> &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<size_t>(it - candidates.begin());
const size_t nextIdx = (idx + 1) % candidates.size();
return candidates[nextIdx].id;
}
[[nodiscard]] std::optional<int> FindPreviousTrackerCandidateId(const std::vector<TrackerCandidate> &candidates, int currentId)
{
if (candidates.empty())
return std::nullopt;
if (currentId < 0)
return candidates.back().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.back().id;
if (candidates.size() <= 1)
return std::nullopt;
const size_t idx = static_cast<size_t>(it - candidates.begin());
const size_t prevIdx = (idx + candidates.size() - 1) % candidates.size();
return candidates[prevIdx].id;
}
void DecorateTrackerTargetNameWithOrdinalIfNeeded(int targetId, StringOrView &targetName, const std::vector<TrackerCandidate> &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;
}
std::optional<int> FindNearestUnopenedChestObjectId(Point playerPosition)
{
return FindNearestObjectId(playerPosition, IsTrackedChestObject);
}
std::optional<int> FindNearestDoorObjectId(Point playerPosition)
{
return FindNearestObjectId(playerPosition, IsTrackedDoorObject);
}
std::optional<int> FindNearestShrineObjectId(Point playerPosition)
{
return FindNearestObjectId(playerPosition, IsShrineLikeObject);
}
std::optional<int> FindNearestBreakableObjectId(Point playerPosition)
{
return FindNearestObjectId(playerPosition, IsTrackedBreakableObject);
}
std::optional<int> FindNearestMiscInteractableObjectId(Point playerPosition)
{
return FindNearestObjectId(playerPosition, IsTrackedMiscInteractableObject);
}
std::optional<int> FindNearestMonsterId(Point playerPosition)
{
std::optional<int> bestId;
int bestDistance = 0;
for (size_t i = 0; i < ActiveMonsterCount; ++i) {
const int monsterId = static_cast<int>(ActiveMonsters[i]);
const Monster &monster = Monsters[monsterId];
if (!IsTrackedMonster(monster))
continue;
const int distance = playerPosition.ApproxDistance(monster.position.future);
if (!bestId || distance < bestDistance) {
bestId = monsterId;
bestDistance = distance;
}
}
return bestId;
}
std::optional<Point> FindBestAdjacentApproachTile(const Player &player, Point playerPosition, Point targetPosition)
{
std::optional<Point> best;
size_t bestPathLength = 0;
int bestDistance = 0;
std::optional<Point> 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<std::vector<int8_t>> 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;
}
std::optional<Point> FindBestApproachTileForObject(const Player &player, Point playerPosition, const Object &object)
{
if (!object._oSolidFlag && PosOkPlayer(player, object.position))
return object.position;
std::optional<Point> best;
size_t bestPathLength = 0;
int bestDistance = 0;
std::optional<Point> 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<std::vector<int8_t>> 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) {
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;
};
std::optional<DoorBlockInfo> 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->_oVar4 == DOOR_CLOSED) {
return DoorBlockInfo { .beforeDoor = position, .doorPosition = object->position };
}
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<TrackerPathBlockInfo> 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->_oVar4 == DOOR_CLOSED) {
return TrackerPathBlockInfo {
.type = TrackerPathBlockType::Door,
.stepIndex = i,
.beforeBlock = position,
.blockPosition = object->position,
};
}
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<int>(MaxMonsters) && !Monsters[monsterId].hasNoLife());
if (blocks) {
return TrackerPathBlockInfo {
.type = TrackerPathBlockType::Monster,
.stepIndex = i,
.beforeBlock = position,
.blockPosition = next,
};
}
}
position = next;
}
return std::nullopt;
}
/**
* Validates an object-category auto-walk target and computes the walk destination.
*/
template <typename Predicate>
bool ValidateAutoWalkObjectTarget(
const Player &myPlayer, Point playerPosition,
Predicate isValid, const char *goneMessage, const char *inRangeMessage,
std::optional<Point> &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) <= TrackerObjectInteractDistance(object)) {
AutoWalkTrackerTargetId = -1;
SpeakText(_(inRangeMessage), true);
return false;
}
destination = FindBestApproachTileForObject(myPlayer, playerPosition, object);
return true;
}
/**
* Resolves which object to walk toward for the given tracker category.
*/
template <typename Predicate, typename FindNearest, typename GetName>
std::optional<int> ResolveObjectTrackerTarget(
int &lockedTargetId, Point playerPosition,
Predicate isValid, FindNearest findNearest, GetName getName,
const char *notFoundMessage, StringOrView &targetName)
{
std::optional<int> 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;
}
if (!isValid(Objects[*targetId])) {
SpeakText(_(notFoundMessage), true);
return std::nullopt;
}
}
lockedTargetId = *targetId;
targetName = getName(*targetId);
return targetId;
}
[[nodiscard]] std::vector<TrackerTargetCategory> TrackerTargetCategoriesForCurrentLevel()
{
if (leveltype == DTYPE_TOWN) {
return {
TrackerTargetCategory::Items,
TrackerTargetCategory::DeadBodies,
TrackerTargetCategory::Npcs,
TrackerTargetCategory::Players,
TrackerTargetCategory::DungeonEntrances,
TrackerTargetCategory::Portals,
};
}
return {
TrackerTargetCategory::Items,
TrackerTargetCategory::Chests,
TrackerTargetCategory::Doors,
TrackerTargetCategory::Shrines,
TrackerTargetCategory::Objects,
TrackerTargetCategory::Breakables,
TrackerTargetCategory::Monsters,
TrackerTargetCategory::DeadBodies,
TrackerTargetCategory::DungeonEntrances,
TrackerTargetCategory::Stairs,
TrackerTargetCategory::QuestLocations,
TrackerTargetCategory::Players,
TrackerTargetCategory::Portals,
};
}
void SelectTrackerTargetCategoryRelative(int delta)
{
if (!CanPlayerTakeAction() || InGameMenu())
return;
AutoWalkTrackerTargetId = -1;
const std::vector<TrackerTargetCategory> categories = TrackerTargetCategoriesForCurrentLevel();
if (categories.empty())
return;
auto it = std::find(categories.begin(), categories.end(), SelectedTrackerTargetCategory);
int currentIndex = 0;
if (it == categories.end()) {
currentIndex = delta > 0 ? -1 : 0;
} else {
currentIndex = static_cast<int>(it - categories.begin());
}
const int count = static_cast<int>(categories.size());
int newIndex = (currentIndex + delta) % count;
if (newIndex < 0)
newIndex += count;
SelectedTrackerTargetCategory = categories[static_cast<size_t>(newIndex)];
SpeakTrackerTargetCategory();
}
[[nodiscard]] std::vector<TrackerCandidate> CollectTrackerCandidatesForSelection(TrackerTargetCategory category, Point playerPosition)
{
switch (category) {
case TrackerTargetCategory::Items:
return CollectNearbyItemTrackerCandidates(playerPosition, TrackerCycleDistanceTiles);
case TrackerTargetCategory::Chests:
return CollectNearbyChestTrackerCandidates(playerPosition, TrackerCycleDistanceTiles);
case TrackerTargetCategory::Doors: {
std::vector<TrackerCandidate> candidates = CollectNearbyDoorTrackerCandidates(playerPosition, TrackerCycleDistanceTiles);
for (TrackerCandidate &c : candidates) {
if (c.id < 0 || c.id >= MAXOBJECTS)
continue;
c.name = DoorLabelForSpeech(Objects[c.id]);
}
return candidates;
}
case TrackerTargetCategory::Shrines:
return CollectNearbyShrineTrackerCandidates(playerPosition, TrackerCycleDistanceTiles);
case TrackerTargetCategory::Objects:
return CollectNearbyObjectInteractableTrackerCandidates(playerPosition, TrackerCycleDistanceTiles);
case TrackerTargetCategory::Breakables:
return CollectNearbyBreakableTrackerCandidates(playerPosition, TrackerCycleDistanceTiles);
case TrackerTargetCategory::Monsters:
return CollectNearbyMonsterTrackerCandidates(playerPosition, TrackerCycleDistanceTiles);
case TrackerTargetCategory::DeadBodies:
return CollectNearbyCorpseTrackerCandidates(playerPosition, TrackerCycleDistanceTiles);
case TrackerTargetCategory::Npcs:
return CollectNpcTrackerCandidates(playerPosition);
case TrackerTargetCategory::Players:
return CollectPlayerTrackerCandidates(playerPosition);
case TrackerTargetCategory::DungeonEntrances:
return CollectDungeonEntranceTrackerCandidates(playerPosition);
case TrackerTargetCategory::Stairs:
return CollectStairsTrackerCandidates(playerPosition);
case TrackerTargetCategory::QuestLocations:
return CollectQuestLocationTrackerCandidates(playerPosition);
case TrackerTargetCategory::Portals:
return CollectPortalTrackerCandidates(playerPosition);
}
app_fatal("Invalid TrackerTargetCategory");
}
[[nodiscard]] std::string_view TrackerCategoryNoCandidatesFoundMessage(TrackerTargetCategory category)
{
switch (category) {
case TrackerTargetCategory::Items:
return _("No items found.");
case TrackerTargetCategory::Chests:
return _("No chests found.");
case TrackerTargetCategory::Doors:
return _("No doors found.");
case TrackerTargetCategory::Shrines:
return _("No shrines found.");
case TrackerTargetCategory::Objects:
return _("No objects found.");
case TrackerTargetCategory::Breakables:
return _("No breakables found.");
case TrackerTargetCategory::Monsters:
return _("No monsters found.");
case TrackerTargetCategory::DeadBodies:
return _("No dead bodies found.");
case TrackerTargetCategory::Npcs:
return _("No NPCs found.");
case TrackerTargetCategory::Players:
return _("No players found.");
case TrackerTargetCategory::DungeonEntrances:
if (leveltype != DTYPE_TOWN)
return _("No exits found.");
return _("No dungeon entrances found.");
case TrackerTargetCategory::Stairs:
return _("No stairs found.");
case TrackerTargetCategory::QuestLocations:
return _("No quest locations found.");
case TrackerTargetCategory::Portals:
return _("No portals found.");
}
app_fatal("Invalid TrackerTargetCategory");
}
[[nodiscard]] constexpr bool TrackerCategorySelectionIsProximityLimited(TrackerTargetCategory category)
{
return IsAnyOf(category, TrackerTargetCategory::Items, TrackerTargetCategory::Chests, TrackerTargetCategory::Doors, TrackerTargetCategory::Shrines, TrackerTargetCategory::Objects,
TrackerTargetCategory::Breakables, TrackerTargetCategory::Monsters, TrackerTargetCategory::DeadBodies);
}
[[nodiscard]] bool TrackerCategoryHasAnyTargets(TrackerTargetCategory category, Point playerPosition)
{
switch (category) {
case TrackerTargetCategory::Items:
return FindNearestGroundItemId(playerPosition).has_value();
case TrackerTargetCategory::Chests:
return FindNearestUnopenedChestObjectId(playerPosition).has_value();
case TrackerTargetCategory::Doors:
return FindNearestDoorObjectId(playerPosition).has_value();
case TrackerTargetCategory::Shrines:
return FindNearestShrineObjectId(playerPosition).has_value();
case TrackerTargetCategory::Objects:
return FindNearestMiscInteractableObjectId(playerPosition).has_value();
case TrackerTargetCategory::Breakables:
return FindNearestBreakableObjectId(playerPosition).has_value();
case TrackerTargetCategory::Monsters:
return FindNearestMonsterId(playerPosition).has_value();
case TrackerTargetCategory::DeadBodies:
return FindNearestCorpseId(playerPosition).has_value();
default:
return false;
}
}
[[nodiscard]] std::string_view TrackerCategoryNoNearbyCandidatesFoundMessage(TrackerTargetCategory category)
{
switch (category) {
case TrackerTargetCategory::Items:
return _("No nearby items found.");
case TrackerTargetCategory::Chests:
return _("No nearby chests found.");
case TrackerTargetCategory::Doors:
return _("No nearby doors found.");
case TrackerTargetCategory::Shrines:
return _("No nearby shrines found.");
case TrackerTargetCategory::Objects:
return _("No nearby objects found.");
case TrackerTargetCategory::Breakables:
return _("No nearby breakables found.");
case TrackerTargetCategory::Monsters:
return _("No nearby monsters found.");
case TrackerTargetCategory::DeadBodies:
return _("No nearby dead bodies found.");
default:
return TrackerCategoryNoCandidatesFoundMessage(category);
}
}
[[nodiscard]] std::string_view TrackerCategoryNoNextMessage(TrackerTargetCategory category)
{
switch (category) {
case TrackerTargetCategory::Items:
return _("No next item.");
case TrackerTargetCategory::Chests:
return _("No next chest.");
case TrackerTargetCategory::Doors:
return _("No next door.");
case TrackerTargetCategory::Shrines:
return _("No next shrine.");
case TrackerTargetCategory::Objects:
return _("No next object.");
case TrackerTargetCategory::Breakables:
return _("No next breakable.");
case TrackerTargetCategory::Monsters:
return _("No next monster.");
case TrackerTargetCategory::DeadBodies:
return _("No next dead body.");
case TrackerTargetCategory::Npcs:
return _("No next NPC.");
case TrackerTargetCategory::Players:
return _("No next player.");
case TrackerTargetCategory::DungeonEntrances:
return _("No next dungeon entrance.");
case TrackerTargetCategory::Stairs:
return _("No next stairs.");
case TrackerTargetCategory::QuestLocations:
return _("No next quest location.");
case TrackerTargetCategory::Portals:
return _("No next portal.");
}
app_fatal("Invalid TrackerTargetCategory");
}
[[nodiscard]] std::string_view TrackerCategoryNoPreviousMessage(TrackerTargetCategory category)
{
switch (category) {
case TrackerTargetCategory::Items:
return _("No previous item.");
case TrackerTargetCategory::Chests:
return _("No previous chest.");
case TrackerTargetCategory::Doors:
return _("No previous door.");
case TrackerTargetCategory::Shrines:
return _("No previous shrine.");
case TrackerTargetCategory::Objects:
return _("No previous object.");
case TrackerTargetCategory::Breakables:
return _("No previous breakable.");
case TrackerTargetCategory::Monsters:
return _("No previous monster.");
case TrackerTargetCategory::DeadBodies:
return _("No previous dead body.");
case TrackerTargetCategory::Npcs:
return _("No previous NPC.");
case TrackerTargetCategory::Players:
return _("No previous player.");
case TrackerTargetCategory::DungeonEntrances:
return _("No previous dungeon entrance.");
case TrackerTargetCategory::Stairs:
return _("No previous stairs.");
case TrackerTargetCategory::QuestLocations:
return _("No previous quest location.");
case TrackerTargetCategory::Portals:
return _("No previous portal.");
}
app_fatal("Invalid TrackerTargetCategory");
}
/**
* Returns true if the given tracker category requires a dungeon (i.e. is not
* available in town).
*/
[[nodiscard]] bool IsDungeonOnlyTrackerCategory(TrackerTargetCategory category)
{
return IsNoneOf(category, TrackerTargetCategory::Items, TrackerTargetCategory::DeadBodies,
TrackerTargetCategory::Npcs, TrackerTargetCategory::Players,
TrackerTargetCategory::DungeonEntrances, TrackerTargetCategory::Portals);
}
void SelectTrackerTargetRelative(int delta)
{
if (!CanPlayerTakeAction() || InGameMenu())
return;
if (MyPlayer == nullptr)
return;
if (leveltype == DTYPE_TOWN && IsDungeonOnlyTrackerCategory(SelectedTrackerTargetCategory)) {
SpeakText(_("Not in a dungeon."), true);
return;
}
if (AutomapActive) {
SpeakText(_("Close the map first."), true);
return;
}
EnsureTrackerLocksMatchCurrentLevel();
const Point playerPosition = MyPlayer->position.future;
AutoWalkTrackerTargetId = -1;
const std::vector<TrackerCandidate> candidates = CollectTrackerCandidatesForSelection(SelectedTrackerTargetCategory, playerPosition);
if (candidates.empty()) {
LockedTrackerTargetId(SelectedTrackerTargetCategory) = -1;
if (TrackerCategorySelectionIsProximityLimited(SelectedTrackerTargetCategory) && TrackerCategoryHasAnyTargets(SelectedTrackerTargetCategory, playerPosition))
SpeakText(TrackerCategoryNoNearbyCandidatesFoundMessage(SelectedTrackerTargetCategory), true);
else
SpeakText(TrackerCategoryNoCandidatesFoundMessage(SelectedTrackerTargetCategory), true);
return;
}
int &lockedTargetId = LockedTrackerTargetId(SelectedTrackerTargetCategory);
if (candidates.size() == 1) {
lockedTargetId = candidates.front().id;
SpeakText(candidates.front().name.str(), /*force=*/true);
return;
}
const std::optional<int> targetId = delta > 0 ? FindNextTrackerCandidateId(candidates, lockedTargetId) : FindPreviousTrackerCandidateId(candidates, lockedTargetId);
if (!targetId) {
SpeakText(delta > 0 ? TrackerCategoryNoNextMessage(SelectedTrackerTargetCategory) : TrackerCategoryNoPreviousMessage(SelectedTrackerTargetCategory), true);
return;
}
const auto it = std::find_if(candidates.begin(), candidates.end(), [id = *targetId](const TrackerCandidate &c) { return c.id == id; });
if (it == candidates.end()) {
lockedTargetId = -1;
SpeakText(TrackerCategoryNoCandidatesFoundMessage(SelectedTrackerTargetCategory), true);
return;
}
lockedTargetId = *targetId;
StringOrView targetName = it->name.str();
DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, candidates);
SpeakText(targetName.str(), /*force=*/true);
}
} // namespace
namespace {
void NavigateToTrackerTargetKeyPressed()
{
if (!CanPlayerTakeAction() || InGameMenu())
return;
if (leveltype == DTYPE_TOWN && IsDungeonOnlyTrackerCategory(SelectedTrackerTargetCategory)) {
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<int> targetId;
std::optional<Point> targetPosition;
std::optional<Point> alternateTargetPosition;
StringOrView targetName;
switch (SelectedTrackerTargetCategory) {
case TrackerTargetCategory::Items: {
const std::vector<TrackerCandidate> 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<TrackerCandidate> 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<TrackerCandidate> 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<TrackerCandidate> 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<TrackerCandidate> 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 = TrackerObjectLabelForSpeech(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::Breakables: {
const std::vector<TrackerCandidate> 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<TrackerCandidate> 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<int>(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 = MonsterLabelForSpeech(tracked);
DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates);
if (!cycleTarget) {
targetPosition = tracked.position.tile;
}
break;
}
case TrackerTargetCategory::DeadBodies: {
const std::vector<TrackerCandidate> 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;
}
case TrackerTargetCategory::Npcs: {
const std::vector<TrackerCandidate> nearbyCandidates = CollectNpcTrackerCandidates(playerPosition);
if (cycleTarget) {
targetId = FindNextTrackerCandidateId(nearbyCandidates, lockedTargetId);
if (!targetId) {
if (nearbyCandidates.empty())
SpeakText(_("No NPCs found."), true);
else
SpeakText(_("No next NPC."), true);
return;
}
} else if (lockedTargetId >= 0 && lockedTargetId < static_cast<int>(GetNumTowners())) {
targetId = lockedTargetId;
} else if (!nearbyCandidates.empty()) {
targetId = nearbyCandidates.front().id;
}
if (!targetId) {
SpeakText(_("No NPCs found."), true);
return;
}
const auto it = std::find_if(nearbyCandidates.begin(), nearbyCandidates.end(), [id = *targetId](const TrackerCandidate &c) { return c.id == id; });
if (it == nearbyCandidates.end()) {
lockedTargetId = -1;
SpeakText(_("No NPCs found."), true);
return;
}
lockedTargetId = *targetId;
targetName = Towners[*targetId].name;
DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates);
if (!cycleTarget) {
targetPosition = Towners[*targetId].position;
}
break;
}
case TrackerTargetCategory::Players: {
const std::vector<TrackerCandidate> nearbyCandidates = CollectPlayerTrackerCandidates(playerPosition);
if (cycleTarget) {
targetId = FindNextTrackerCandidateId(nearbyCandidates, lockedTargetId);
if (!targetId) {
if (nearbyCandidates.empty())
SpeakText(_("No players found."), true);
else
SpeakText(_("No next player."), true);
return;
}
} else if (lockedTargetId >= 0 && lockedTargetId < MAX_PLRS) {
targetId = lockedTargetId;
} else if (!nearbyCandidates.empty()) {
targetId = nearbyCandidates.front().id;
}
if (!targetId) {
SpeakText(_("No players found."), true);
return;
}
const auto it = std::find_if(nearbyCandidates.begin(), nearbyCandidates.end(), [id = *targetId](const TrackerCandidate &c) { return c.id == id; });
if (it == nearbyCandidates.end()) {
lockedTargetId = -1;
SpeakText(_("No players found."), true);
return;
}
lockedTargetId = *targetId;
targetName = Players[*targetId].name();
DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates);
if (!cycleTarget) {
targetPosition = Players[*targetId].position.future;
}
break;
}
case TrackerTargetCategory::DungeonEntrances: {
const std::vector<TrackerCandidate> nearbyCandidates = CollectDungeonEntranceTrackerCandidates(playerPosition);
if (cycleTarget) {
targetId = FindNextTrackerCandidateId(nearbyCandidates, lockedTargetId);
if (!targetId) {
if (nearbyCandidates.empty())
SpeakText(_("No dungeon entrances found."), true);
else
SpeakText(_("No next dungeon entrance."), true);
return;
}
} else if (!nearbyCandidates.empty()) {
const auto lockedIt = std::find_if(nearbyCandidates.begin(), nearbyCandidates.end(), [id = lockedTargetId](const TrackerCandidate &c) { return c.id == id; });
targetId = lockedIt != nearbyCandidates.end() ? lockedTargetId : nearbyCandidates.front().id;
}
if (!targetId) {
SpeakText(_("No dungeon entrances found."), true);
return;
}
const auto it = std::find_if(nearbyCandidates.begin(), nearbyCandidates.end(), [id = *targetId](const TrackerCandidate &c) { return c.id == id; });
if (it == nearbyCandidates.end()) {
lockedTargetId = -1;
SpeakText(_("No dungeon entrances found."), true);
return;
}
lockedTargetId = *targetId;
targetName = std::string(it->name.str());
DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates);
if (!cycleTarget) {
if (IsRedPortalTrackerId(*targetId)) {
targetPosition = RedPortalPositionForTrackerId(*targetId);
} else {
const TriggerStruct &trigger = trigs[*targetId];
targetPosition = Point { trigger.position.x, trigger.position.y };
}
}
break;
}
case TrackerTargetCategory::Stairs: {
const std::vector<TrackerCandidate> nearbyCandidates = CollectStairsTrackerCandidates(playerPosition);
if (cycleTarget) {
targetId = FindNextTrackerCandidateId(nearbyCandidates, lockedTargetId);
if (!targetId) {
if (nearbyCandidates.empty())
SpeakText(_("No stairs found."), true);
else
SpeakText(_("No next stairs."), true);
return;
}
} else if (lockedTargetId >= 0 && lockedTargetId < numtrigs) {
targetId = lockedTargetId;
} else if (!nearbyCandidates.empty()) {
targetId = nearbyCandidates.front().id;
}
if (!targetId) {
SpeakText(_("No stairs found."), true);
return;
}
const auto it = std::find_if(nearbyCandidates.begin(), nearbyCandidates.end(), [id = *targetId](const TrackerCandidate &c) { return c.id == id; });
if (it == nearbyCandidates.end()) {
lockedTargetId = -1;
SpeakText(_("No stairs found."), true);
return;
}
lockedTargetId = *targetId;
targetName = std::string(it->name.str());
DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates);
if (!cycleTarget) {
const TriggerStruct &trigger = trigs[*targetId];
targetPosition = Point { trigger.position.x, trigger.position.y };
}
break;
}
case TrackerTargetCategory::QuestLocations: {
const std::vector<TrackerCandidate> nearbyCandidates = CollectQuestLocationTrackerCandidates(playerPosition);
if (cycleTarget) {
targetId = FindNextTrackerCandidateId(nearbyCandidates, lockedTargetId);
if (!targetId) {
if (nearbyCandidates.empty())
SpeakText(_("No quest locations found."), true);
else
SpeakText(_("No next quest location."), true);
return;
}
} else if (!nearbyCandidates.empty()) {
const auto lockedIt = std::find_if(nearbyCandidates.begin(), nearbyCandidates.end(), [id = lockedTargetId](const TrackerCandidate &c) { return c.id == id; });
targetId = lockedIt != nearbyCandidates.end() ? lockedTargetId : nearbyCandidates.front().id;
}
if (!targetId) {
SpeakText(_("No quest locations found."), true);
return;
}
const auto it = std::find_if(nearbyCandidates.begin(), nearbyCandidates.end(), [id = *targetId](const TrackerCandidate &c) { return c.id == id; });
if (it == nearbyCandidates.end()) {
lockedTargetId = -1;
SpeakText(_("No quest locations found."), true);
return;
}
lockedTargetId = *targetId;
targetName = std::string(it->name.str());
DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates);
if (!cycleTarget) {
if (setlevel) {
if (IsRedPortalTrackerId(*targetId)) {
targetPosition = RedPortalPositionForTrackerId(*targetId);
} else {
const TriggerStruct &trigger = trigs[*targetId];
targetPosition = Point { trigger.position.x, trigger.position.y };
}
} else {
const Quest &quest = Quests[static_cast<size_t>(*targetId)];
targetPosition = quest.position;
}
}
break;
}
case TrackerTargetCategory::Portals: {
const std::vector<TrackerCandidate> nearbyCandidates = CollectPortalTrackerCandidates(playerPosition);
if (cycleTarget) {
targetId = FindNextTrackerCandidateId(nearbyCandidates, lockedTargetId);
if (!targetId) {
if (nearbyCandidates.empty())
SpeakText(_("No portals found."), true);
else
SpeakText(_("No next portal."), true);
return;
}
} else if (lockedTargetId >= 0 && lockedTargetId < MAXPORTAL) {
targetId = lockedTargetId;
} else if (!nearbyCandidates.empty()) {
targetId = nearbyCandidates.front().id;
}
if (!targetId) {
SpeakText(_("No portals found."), true);
return;
}
const auto it = std::find_if(nearbyCandidates.begin(), nearbyCandidates.end(), [id = *targetId](const TrackerCandidate &c) { return c.id == id; });
if (it == nearbyCandidates.end()) {
lockedTargetId = -1;
SpeakText(_("No portals found."), true);
return;
}
Point portalPosition;
if (leveltype == DTYPE_TOWN) {
const std::optional<Point> townPos = FindTownPortalPositionInTownByPortalIndex(*targetId);
if (!townPos) {
lockedTargetId = -1;
SpeakText(_("No portals found."), true);
return;
}
portalPosition = *townPos;
} else {
if (!IsTownPortalOpenOnCurrentLevel(*targetId)) {
lockedTargetId = -1;
SpeakText(_("No portals found."), true);
return;
}
portalPosition = Portals[*targetId].position;
}
lockedTargetId = *targetId;
targetName = TownPortalLabelForSpeech(Portals[*targetId]);
DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates);
if (!cycleTarget) {
targetPosition = portalPosition;
}
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<std::vector<int8_t>> {
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<std::vector<int8_t>> spokenPath;
bool pathIgnoresDoors = false;
bool pathIgnoresMonsters = false;
bool pathIgnoresBreakables = false;
const auto considerDestination = [&](Point destination, TrackerPathMode mode) {
const std::optional<std::vector<int8_t>> 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<TrackerPathBlockInfo> 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<int8_t>(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);
}
void AutoWalkToTrackerTargetKeyPressed()
{
if (AutoWalkTrackerTargetId >= 0) {
CancelAutoWalk();
SpeakText(_("Walk cancelled."), true);
return;
}
if (!CanPlayerTakeAction() || InGameMenu())
return;
if (leveltype == DTYPE_TOWN && IsDungeonOnlyTrackerCategory(SelectedTrackerTargetCategory)) {
SpeakText(_("Not in a dungeon."), true);
return;
}
if (AutomapActive) {
SpeakText(_("Close the map first."), true);
return;
}
if (MyPlayer == nullptr) {
AutoWalkTrackerTargetId = -1;
SpeakText(_("Cannot walk right now."), true);
return;
}
EnsureTrackerLocksMatchCurrentLevel();
const Point playerPosition = MyPlayer->position.future;
int &lockedTargetId = LockedTrackerTargetId(SelectedTrackerTargetCategory);
std::optional<int> 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 TrackerObjectLabelForSpeech(Objects[id]); }, 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<int>(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;
}
case TrackerTargetCategory::Npcs: {
const std::vector<TrackerCandidate> candidates = CollectNpcTrackerCandidates(playerPosition);
if (candidates.empty()) {
SpeakText(_("No NPCs found."), true);
return;
}
if (lockedTargetId >= 0 && lockedTargetId < static_cast<int>(GetNumTowners())) {
const auto it = std::find_if(candidates.begin(), candidates.end(), [id = lockedTargetId](const TrackerCandidate &c) { return c.id == id; });
if (it != candidates.end())
targetId = lockedTargetId;
}
if (!targetId)
targetId = candidates.front().id;
lockedTargetId = *targetId;
targetName = Towners[*targetId].name;
break;
}
case TrackerTargetCategory::Players: {
const std::vector<TrackerCandidate> candidates = CollectPlayerTrackerCandidates(playerPosition);
if (candidates.empty()) {
SpeakText(_("No players found."), true);
return;
}
if (lockedTargetId >= 0 && lockedTargetId < MAX_PLRS) {
const auto it = std::find_if(candidates.begin(), candidates.end(), [id = lockedTargetId](const TrackerCandidate &c) { return c.id == id; });
if (it != candidates.end())
targetId = lockedTargetId;
}
if (!targetId)
targetId = candidates.front().id;
lockedTargetId = *targetId;
targetName = Players[*targetId].name();
break;
}
case TrackerTargetCategory::DungeonEntrances: {
const std::vector<TrackerCandidate> candidates = CollectDungeonEntranceTrackerCandidates(playerPosition);
if (candidates.empty()) {
SpeakText(_("No dungeon entrances found."), true);
return;
}
if (lockedTargetId >= 0 && lockedTargetId < numtrigs) {
const auto it = std::find_if(candidates.begin(), candidates.end(), [id = lockedTargetId](const TrackerCandidate &c) { return c.id == id; });
if (it != candidates.end())
targetId = lockedTargetId;
}
if (!targetId)
targetId = candidates.front().id;
lockedTargetId = *targetId;
targetName = TriggerLabelForSpeech(trigs[*targetId]);
break;
}
case TrackerTargetCategory::Stairs: {
const std::vector<TrackerCandidate> candidates = CollectStairsTrackerCandidates(playerPosition);
if (candidates.empty()) {
SpeakText(_("No stairs found."), true);
return;
}
if (lockedTargetId >= 0 && lockedTargetId < numtrigs) {
const auto it = std::find_if(candidates.begin(), candidates.end(), [id = lockedTargetId](const TrackerCandidate &c) { return c.id == id; });
if (it != candidates.end())
targetId = lockedTargetId;
}
if (!targetId)
targetId = candidates.front().id;
lockedTargetId = *targetId;
targetName = TriggerLabelForSpeech(trigs[*targetId]);
break;
}
case TrackerTargetCategory::QuestLocations: {
const std::vector<TrackerCandidate> candidates = CollectQuestLocationTrackerCandidates(playerPosition);
if (candidates.empty()) {
SpeakText(_("No quest locations found."), true);
return;
}
if ((setlevel && lockedTargetId >= 0 && lockedTargetId < numtrigs) || (!setlevel && lockedTargetId >= 0 && lockedTargetId < static_cast<int>(sizeof(Quests) / sizeof(Quests[0])))) {
const auto it = std::find_if(candidates.begin(), candidates.end(), [id = lockedTargetId](const TrackerCandidate &c) { return c.id == id; });
if (it != candidates.end())
targetId = lockedTargetId;
}
if (!targetId)
targetId = candidates.front().id;
lockedTargetId = *targetId;
targetName = std::string(candidates.front().name.str());
if (const auto it = std::find_if(candidates.begin(), candidates.end(), [id = *targetId](const TrackerCandidate &c) { return c.id == id; }); it != candidates.end())
targetName = std::string(it->name.str());
break;
}
case TrackerTargetCategory::Portals: {
const std::vector<TrackerCandidate> candidates = CollectPortalTrackerCandidates(playerPosition);
if (candidates.empty()) {
SpeakText(_("No portals found."), true);
return;
}
if (lockedTargetId >= 0 && lockedTargetId < MAXPORTAL) {
const auto it = std::find_if(candidates.begin(), candidates.end(), [id = lockedTargetId](const TrackerCandidate &c) { return c.id == id; });
if (it != candidates.end())
targetId = lockedTargetId;
}
if (!targetId)
targetId = candidates.front().id;
lockedTargetId = *targetId;
targetName = TownPortalLabelForSpeech(Portals[*targetId]);
break;
}
}
if (!targetId)
return;
std::string msg;
StrAppend(msg, _("Going to: "), targetName);
SpeakText(msg, true);
AutoWalkTrackerTargetId = *targetId;
AutoWalkTrackerTargetCategory = SelectedTrackerTargetCategory;
UpdateAutoWalkTracker();
}
} // namespace
void UpdateAutoWalkTracker()
{
if (AutoWalkTrackerTargetId < 0)
return;
if (IsPlayerInStore() || ChatLogFlag || HelpFlag || InGameMenu()) {
AutoWalkTrackerTargetId = -1;
return;
}
if (leveltype == DTYPE_TOWN
&& IsDungeonOnlyTrackerCategory(AutoWalkTrackerTargetCategory)) {
AutoWalkTrackerTargetId = -1;
return;
}
if (!CanPlayerTakeAction())
return;
if (MyPlayer == nullptr) {
AutoWalkTrackerTargetId = -1;
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<Point> 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<int>(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;
}
case TrackerTargetCategory::Npcs: {
const int npcId = AutoWalkTrackerTargetId;
if (leveltype != DTYPE_TOWN || npcId < 0 || npcId >= static_cast<int>(GetNumTowners())) {
AutoWalkTrackerTargetId = -1;
SpeakText(_("Target NPC is gone."), true);
return;
}
const Towner &towner = Towners[npcId];
if (!IsTownerPresent(towner._ttype)) {
AutoWalkTrackerTargetId = -1;
SpeakText(_("Target NPC is gone."), true);
return;
}
if (playerPosition.WalkingDistance(towner.position) <= TrackerInteractDistanceTiles) {
AutoWalkTrackerTargetId = -1;
SpeakText(_("NPC in range."), true);
return;
}
destination = FindBestAdjacentApproachTile(myPlayer, playerPosition, towner.position);
break;
}
case TrackerTargetCategory::Players: {
const int playerId = AutoWalkTrackerTargetId;
if (playerId < 0 || playerId >= MAX_PLRS) {
AutoWalkTrackerTargetId = -1;
SpeakText(_("Target player is gone."), true);
return;
}
const Player &player = Players[playerId];
if (!player.plractive || player._pLvlChanging || player.plrIsOnSetLevel != setlevel || player.plrlevel != MyPlayer->plrlevel) {
AutoWalkTrackerTargetId = -1;
SpeakText(_("Target player is gone."), true);
return;
}
const Point targetPosition = player.position.future;
if (!InDungeonBounds(targetPosition)) {
AutoWalkTrackerTargetId = -1;
SpeakText(_("Target player is gone."), true);
return;
}
if (playerPosition.WalkingDistance(targetPosition) <= TrackerInteractDistanceTiles) {
AutoWalkTrackerTargetId = -1;
SpeakText(_("Player in range."), true);
return;
}
destination = FindBestAdjacentApproachTile(myPlayer, playerPosition, targetPosition);
break;
}
case TrackerTargetCategory::DungeonEntrances: {
const int triggerIndex = AutoWalkTrackerTargetId;
if (triggerIndex < 0 || triggerIndex >= numtrigs) {
AutoWalkTrackerTargetId = -1;
SpeakText(_("Target entrance is gone."), true);
return;
}
const TriggerStruct &trigger = trigs[triggerIndex];
const bool valid = leveltype == DTYPE_TOWN
? IsAnyOf(trigger._tmsg, WM_DIABNEXTLVL, WM_DIABTOWNWARP)
: (setlevel ? trigger._tmsg == WM_DIABRTNLVL : IsAnyOf(trigger._tmsg, WM_DIABPREVLVL, WM_DIABTWARPUP));
if (!valid) {
AutoWalkTrackerTargetId = -1;
SpeakText(_("Target entrance is gone."), true);
return;
}
const Point triggerPosition { trigger.position.x, trigger.position.y };
if (playerPosition.WalkingDistance(triggerPosition) <= TrackerInteractDistanceTiles) {
AutoWalkTrackerTargetId = -1;
SpeakText(_("Entrance in range."), true);
return;
}
destination = triggerPosition;
break;
}
case TrackerTargetCategory::Stairs: {
const int triggerIndex = AutoWalkTrackerTargetId;
if (leveltype == DTYPE_TOWN || triggerIndex < 0 || triggerIndex >= numtrigs) {
AutoWalkTrackerTargetId = -1;
SpeakText(_("Target stairs are gone."), true);
return;
}
const TriggerStruct &trigger = trigs[triggerIndex];
if (!IsAnyOf(trigger._tmsg, WM_DIABNEXTLVL, WM_DIABPREVLVL, WM_DIABTWARPUP)) {
AutoWalkTrackerTargetId = -1;
SpeakText(_("Target stairs are gone."), true);
return;
}
const Point triggerPosition { trigger.position.x, trigger.position.y };
if (playerPosition.WalkingDistance(triggerPosition) <= TrackerInteractDistanceTiles) {
AutoWalkTrackerTargetId = -1;
SpeakText(_("Stairs in range."), true);
return;
}
destination = triggerPosition;
break;
}
case TrackerTargetCategory::QuestLocations: {
if (setlevel) {
const int triggerIndex = AutoWalkTrackerTargetId;
if (leveltype == DTYPE_TOWN || triggerIndex < 0 || triggerIndex >= numtrigs) {
AutoWalkTrackerTargetId = -1;
SpeakText(_("Target quest location is gone."), true);
return;
}
const TriggerStruct &trigger = trigs[triggerIndex];
if (trigger._tmsg != WM_DIABRTNLVL) {
AutoWalkTrackerTargetId = -1;
SpeakText(_("Target quest location is gone."), true);
return;
}
const Point triggerPosition { trigger.position.x, trigger.position.y };
if (playerPosition.WalkingDistance(triggerPosition) <= TrackerInteractDistanceTiles) {
AutoWalkTrackerTargetId = -1;
SpeakText(_("Quest exit in range."), true);
return;
}
destination = triggerPosition;
break;
}
const int questIndex = AutoWalkTrackerTargetId;
if (questIndex < 0 || questIndex >= static_cast<int>(sizeof(Quests) / sizeof(Quests[0]))) {
AutoWalkTrackerTargetId = -1;
SpeakText(_("Target quest location is gone."), true);
return;
}
const Quest &quest = Quests[static_cast<size_t>(questIndex)];
if (quest._qslvl == SL_NONE || quest._qactive == QUEST_NOTAVAIL || quest._qlevel != currlevel || !InDungeonBounds(quest.position)) {
AutoWalkTrackerTargetId = -1;
SpeakText(_("Target quest location is gone."), true);
return;
}
if (playerPosition.WalkingDistance(quest.position) <= TrackerInteractDistanceTiles) {
AutoWalkTrackerTargetId = -1;
SpeakText(_("Quest entrance in range."), true);
return;
}
destination = quest.position;
break;
}
case TrackerTargetCategory::Portals: {
const int portalIndex = AutoWalkTrackerTargetId;
std::optional<Point> portalPosition;
if (leveltype == DTYPE_TOWN) {
portalPosition = FindTownPortalPositionInTownByPortalIndex(portalIndex);
} else if (IsTownPortalOpenOnCurrentLevel(portalIndex)) {
portalPosition = Portals[portalIndex].position;
}
if (!portalPosition) {
AutoWalkTrackerTargetId = -1;
SpeakText(_("Target portal is gone."), true);
return;
}
if (playerPosition.WalkingDistance(*portalPosition) <= TrackerInteractDistanceTiles) {
AutoWalkTrackerTargetId = -1;
SpeakText(_("Portal in range."), true);
return;
}
destination = *portalPosition;
break;
}
}
if (!destination) {
AutoWalkTrackerTargetId = -1;
SpeakText(_("Can't find a nearby tile to walk to."), true);
return;
}
constexpr size_t MaxAutoWalkPathLength = 512;
std::array<int8_t, MaxAutoWalkPathLength> path;
path.fill(WALK_NONE);
int steps = FindPath(CanStep, [&myPlayer](Point position) { return PosOkPlayer(myPlayer, position); }, playerPosition, *destination, path.data(), path.size());
if (steps == 0) {
std::array<int8_t, MaxAutoWalkPathLength> 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<DoorBlockInfo> 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;
}
}
if (steps < static_cast<int>(MaxPathLengthPlayer)) {
NetSendCmdLoc(MyPlayerId, true, CMD_WALKXY, *destination);
return;
}
const int segmentSteps = std::min(steps - 1, static_cast<int>(MaxPathLengthPlayer - 1));
const Point waypoint = PositionAfterWalkPathSteps(playerPosition, path.data(), segmentSteps);
NetSendCmdLoc(MyPlayerId, true, CMD_WALKXY, waypoint);
}
void TrackerPageUpKeyPressed()
{
const SDL_Keymod modState = SDL_GetModState();
const bool cycleCategory = (modState & SDL_KMOD_CTRL) != 0;
if (cycleCategory) {
SelectTrackerTargetCategoryRelative(-1);
if (MyPlayer != nullptr) {
const Point playerPosition = MyPlayer->position.future;
if (CollectTrackerCandidatesForSelection(SelectedTrackerTargetCategory, playerPosition).empty()) {
if (TrackerCategorySelectionIsProximityLimited(SelectedTrackerTargetCategory) && TrackerCategoryHasAnyTargets(SelectedTrackerTargetCategory, playerPosition))
SpeakText(TrackerCategoryNoNearbyCandidatesFoundMessage(SelectedTrackerTargetCategory), true);
else
SpeakText(TrackerCategoryNoCandidatesFoundMessage(SelectedTrackerTargetCategory), true);
}
}
return;
}
SelectTrackerTargetRelative(-1);
}
void TrackerPageDownKeyPressed()
{
const SDL_Keymod modState = SDL_GetModState();
const bool cycleCategory = (modState & SDL_KMOD_CTRL) != 0;
if (cycleCategory) {
SelectTrackerTargetCategoryRelative(+1);
if (MyPlayer != nullptr) {
const Point playerPosition = MyPlayer->position.future;
if (CollectTrackerCandidatesForSelection(SelectedTrackerTargetCategory, playerPosition).empty()) {
if (TrackerCategorySelectionIsProximityLimited(SelectedTrackerTargetCategory) && TrackerCategoryHasAnyTargets(SelectedTrackerTargetCategory, playerPosition))
SpeakText(TrackerCategoryNoNearbyCandidatesFoundMessage(SelectedTrackerTargetCategory), true);
else
SpeakText(TrackerCategoryNoCandidatesFoundMessage(SelectedTrackerTargetCategory), true);
}
}
return;
}
SelectTrackerTargetRelative(+1);
}
void TrackerHomeKeyPressed()
{
const SDL_Keymod modState = SDL_GetModState();
const bool autoWalk = (modState & SDL_KMOD_SHIFT) != 0;
if (autoWalk)
AutoWalkToTrackerTargetKeyPressed();
else
NavigateToTrackerTargetKeyPressed();
}
void ResetAutoWalkTracker()
{
AutoWalkTrackerTargetId = -1;
}
} // namespace devilution