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.
1846 lines
56 KiB
1846 lines
56 KiB
|
3 weeks ago
|
#include "accessibility/tracker.hpp"
|
||
|
|
|
||
|
|
#include <algorithm>
|
||
|
|
#include <array>
|
||
|
|
#include <cstdint>
|
||
|
|
#include <cstdlib>
|
||
|
|
#include <limits>
|
||
|
|
#include <optional>
|
||
|
|
#include <string>
|
||
|
|
#include <vector>
|
||
|
|
|
||
|
|
#ifdef USE_SDL3
|
||
|
|
#include <SDL3/SDL_keycode.h>
|
||
|
|
#else
|
||
|
|
#include <SDL.h>
|
||
|
|
#endif
|
||
|
|
|
||
|
|
#include <fmt/format.h>
|
||
|
|
|
||
|
|
#include "accessibility/location_speech.hpp"
|
||
|
|
#include "accessibility/speech.hpp"
|
||
|
|
#include "appfat.h"
|
||
|
|
#include "automap.h"
|
||
|
|
#include "controls/plrctrls.h"
|
||
|
|
#include "diablo.h"
|
||
|
|
#include "engine/path.h"
|
||
|
|
#include "help.h"
|
||
|
|
#include "items.h"
|
||
|
|
#include "levels/gendung.h"
|
||
|
|
#include "levels/tile_properties.hpp"
|
||
|
|
#include "monster.h"
|
||
|
|
#include "multi.h"
|
||
|
|
#include "objects.h"
|
||
|
|
#include "player.h"
|
||
|
|
#include "qol/chatlog.h"
|
||
|
|
#include "stores.h"
|
||
|
|
#include "utils/is_of.hpp"
|
||
|
|
#include "utils/language.h"
|
||
|
|
#include "utils/screen_reader.hpp"
|
||
|
|
#include "utils/sdl_compat.h"
|
||
|
|
#include "utils/str_cat.hpp"
|
||
|
|
|
||
|
|
namespace devilution {
|
||
|
|
|
||
|
|
TrackerTargetCategory SelectedTrackerTargetCategory = TrackerTargetCategory::Items;
|
||
|
|
TrackerTargetCategory AutoWalkTrackerTargetCategory = TrackerTargetCategory::Items;
|
||
|
|
int AutoWalkTrackerTargetId = -1;
|
||
|
|
|
||
|
|
namespace {
|
||
|
|
|
||
|
|
/// Maximum Chebyshev distance (in tiles) at which the player is considered
|
||
|
|
/// close enough to interact with a tracker target.
|
||
|
|
constexpr int TrackerInteractDistanceTiles = 1;
|
||
|
|
constexpr int TrackerCycleDistanceTiles = 12;
|
||
|
|
|
||
|
|
int LockedTrackerItemId = -1;
|
||
|
|
int LockedTrackerChestId = -1;
|
||
|
|
int LockedTrackerDoorId = -1;
|
||
|
|
int LockedTrackerShrineId = -1;
|
||
|
|
int LockedTrackerObjectId = -1;
|
||
|
|
int LockedTrackerBreakableId = -1;
|
||
|
|
int LockedTrackerMonsterId = -1;
|
||
|
|
int LockedTrackerDeadBodyId = -1;
|
||
|
|
|
||
|
|
struct TrackerLevelKey {
|
||
|
|
dungeon_type levelType;
|
||
|
|
int currLevel;
|
||
|
|
bool isSetLevel;
|
||
|
|
int setLevelNum;
|
||
|
|
};
|
||
|
|
|
||
|
|
std::optional<TrackerLevelKey> LockedTrackerLevelKey;
|
||
|
|
|
||
|
|
void ClearTrackerLocks()
|
||
|
|
{
|
||
|
|
LockedTrackerItemId = -1;
|
||
|
|
LockedTrackerChestId = -1;
|
||
|
|
LockedTrackerDoorId = -1;
|
||
|
|
LockedTrackerShrineId = -1;
|
||
|
|
LockedTrackerObjectId = -1;
|
||
|
|
LockedTrackerBreakableId = -1;
|
||
|
|
LockedTrackerMonsterId = -1;
|
||
|
|
LockedTrackerDeadBodyId = -1;
|
||
|
|
}
|
||
|
|
|
||
|
|
void EnsureTrackerLocksMatchCurrentLevel()
|
||
|
|
{
|
||
|
|
const TrackerLevelKey current {
|
||
|
|
.levelType = leveltype,
|
||
|
|
.currLevel = currlevel,
|
||
|
|
.isSetLevel = setlevel,
|
||
|
|
.setLevelNum = setlvlnum,
|
||
|
|
};
|
||
|
|
|
||
|
|
if (!LockedTrackerLevelKey || LockedTrackerLevelKey->levelType != current.levelType || LockedTrackerLevelKey->currLevel != current.currLevel
|
||
|
|
|| LockedTrackerLevelKey->isSetLevel != current.isSetLevel || LockedTrackerLevelKey->setLevelNum != current.setLevelNum) {
|
||
|
|
ClearTrackerLocks();
|
||
|
|
LockedTrackerLevelKey = current;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
int &LockedTrackerTargetId(TrackerTargetCategory category)
|
||
|
|
{
|
||
|
|
switch (category) {
|
||
|
|
case TrackerTargetCategory::Items:
|
||
|
|
return LockedTrackerItemId;
|
||
|
|
case TrackerTargetCategory::Chests:
|
||
|
|
return LockedTrackerChestId;
|
||
|
|
case TrackerTargetCategory::Doors:
|
||
|
|
return LockedTrackerDoorId;
|
||
|
|
case TrackerTargetCategory::Shrines:
|
||
|
|
return LockedTrackerShrineId;
|
||
|
|
case TrackerTargetCategory::Objects:
|
||
|
|
return LockedTrackerObjectId;
|
||
|
|
case TrackerTargetCategory::Breakables:
|
||
|
|
return LockedTrackerBreakableId;
|
||
|
|
case TrackerTargetCategory::Monsters:
|
||
|
|
return LockedTrackerMonsterId;
|
||
|
|
case TrackerTargetCategory::DeadBodies:
|
||
|
|
return LockedTrackerDeadBodyId;
|
||
|
|
}
|
||
|
|
app_fatal("Invalid TrackerTargetCategory");
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string_view TrackerTargetCategoryLabel(TrackerTargetCategory category)
|
||
|
|
{
|
||
|
|
switch (category) {
|
||
|
|
case TrackerTargetCategory::Items:
|
||
|
|
return _("items");
|
||
|
|
case TrackerTargetCategory::Chests:
|
||
|
|
return _("chests");
|
||
|
|
case TrackerTargetCategory::Doors:
|
||
|
|
return _("doors");
|
||
|
|
case TrackerTargetCategory::Shrines:
|
||
|
|
return _("shrines");
|
||
|
|
case TrackerTargetCategory::Objects:
|
||
|
|
return _("objects");
|
||
|
|
case TrackerTargetCategory::Breakables:
|
||
|
|
return _("breakables");
|
||
|
|
case TrackerTargetCategory::Monsters:
|
||
|
|
return _("monsters");
|
||
|
|
case TrackerTargetCategory::DeadBodies:
|
||
|
|
return _("dead bodies");
|
||
|
|
default:
|
||
|
|
return _("items");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void SpeakTrackerTargetCategory()
|
||
|
|
{
|
||
|
|
std::string message;
|
||
|
|
StrAppend(message, _("Tracker target: "), TrackerTargetCategoryLabel(SelectedTrackerTargetCategory));
|
||
|
|
SpeakText(message, true);
|
||
|
|
}
|
||
|
|
|
||
|
|
[[nodiscard]] constexpr int CorpseTrackerIdForPosition(Point position)
|
||
|
|
{
|
||
|
|
return position.x + position.y * MAXDUNX;
|
||
|
|
}
|
||
|
|
|
||
|
|
[[nodiscard]] constexpr Point CorpsePositionForTrackerId(int corpseId)
|
||
|
|
{
|
||
|
|
return { corpseId % MAXDUNX, corpseId / MAXDUNX };
|
||
|
|
}
|
||
|
|
|
||
|
|
Point NextPositionForWalkDirection(Point position, int8_t walkDir)
|
||
|
|
{
|
||
|
|
switch (walkDir) {
|
||
|
|
case WALK_NE:
|
||
|
|
return { position.x, position.y - 1 };
|
||
|
|
case WALK_NW:
|
||
|
|
return { position.x - 1, position.y };
|
||
|
|
case WALK_SE:
|
||
|
|
return { position.x + 1, position.y };
|
||
|
|
case WALK_SW:
|
||
|
|
return { position.x, position.y + 1 };
|
||
|
|
case WALK_N:
|
||
|
|
return { position.x - 1, position.y - 1 };
|
||
|
|
case WALK_E:
|
||
|
|
return { position.x + 1, position.y - 1 };
|
||
|
|
case WALK_S:
|
||
|
|
return { position.x + 1, position.y + 1 };
|
||
|
|
case WALK_W:
|
||
|
|
return { position.x - 1, position.y + 1 };
|
||
|
|
default:
|
||
|
|
return position;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
Point PositionAfterWalkPathSteps(Point start, const int8_t *path, int steps)
|
||
|
|
{
|
||
|
|
Point position = start;
|
||
|
|
for (int i = 0; i < steps; ++i) {
|
||
|
|
position = NextPositionForWalkDirection(position, path[i]);
|
||
|
|
}
|
||
|
|
return position;
|
||
|
|
}
|
||
|
|
|
||
|
|
[[nodiscard]] std::optional<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]] 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]] 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 = item.getName(),
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
std::sort(result.begin(), result.end(), [](const TrackerCandidate &a, const TrackerCandidate &b) { return IsBetterTrackerCandidate(a, b); });
|
||
|
|
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(), [](const TrackerCandidate &a, const TrackerCandidate &b) { return IsBetterTrackerCandidate(a, b); });
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
[[nodiscard]] constexpr bool IsTrackedChestObject(const Object &object)
|
||
|
|
{
|
||
|
|
return object.canInteractWith() && (object.IsChest() || object._otype == _object_id::OBJ_SIGNCHEST);
|
||
|
|
}
|
||
|
|
|
||
|
|
[[nodiscard]] constexpr bool IsTrackedDoorObject(const Object &object)
|
||
|
|
{
|
||
|
|
// Track both closed and open doors (to match proximity audio cues).
|
||
|
|
return object.isDoor() && object.canInteractWith();
|
||
|
|
}
|
||
|
|
|
||
|
|
[[nodiscard]] constexpr bool IsShrineLikeObject(const Object &object)
|
||
|
|
{
|
||
|
|
return object.canInteractWith()
|
||
|
|
&& (object.IsShrine()
|
||
|
|
|| IsAnyOf(object._otype, _object_id::OBJ_BLOODFTN, _object_id::OBJ_PURIFYINGFTN, _object_id::OBJ_GOATSHRINE, _object_id::OBJ_CAULDRON,
|
||
|
|
_object_id::OBJ_MURKYFTN, _object_id::OBJ_TEARFTN));
|
||
|
|
}
|
||
|
|
|
||
|
|
[[nodiscard]] constexpr bool IsTrackedBreakableObject(const Object &object)
|
||
|
|
{
|
||
|
|
return object.IsBreakable();
|
||
|
|
}
|
||
|
|
|
||
|
|
[[nodiscard]] constexpr bool IsTrackedMiscInteractableObject(const Object &object)
|
||
|
|
{
|
||
|
|
if (!object.canInteractWith())
|
||
|
|
return false;
|
||
|
|
if (object.IsChest() || object._otype == _object_id::OBJ_SIGNCHEST)
|
||
|
|
return false;
|
||
|
|
if (object.isDoor())
|
||
|
|
return false;
|
||
|
|
if (IsShrineLikeObject(object))
|
||
|
|
return false;
|
||
|
|
if (object.IsBreakable())
|
||
|
|
return false;
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
[[nodiscard]] bool IsTrackedMonster(const Monster &monster)
|
||
|
|
{
|
||
|
|
return !monster.isInvalid
|
||
|
|
&& (monster.flags & MFLAG_HIDDEN) == 0
|
||
|
|
&& monster.hitPoints > 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
template <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 = object.name(),
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
std::sort(result.begin(), result.end(), [](const TrackerCandidate &a, const TrackerCandidate &b) { return IsBetterTrackerCandidate(a, b); });
|
||
|
|
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 (monster.isInvalid)
|
||
|
|
continue;
|
||
|
|
if ((monster.flags & MFLAG_HIDDEN) != 0)
|
||
|
|
continue;
|
||
|
|
if (monster.hitPoints <= 0)
|
||
|
|
continue;
|
||
|
|
|
||
|
|
const Point monsterDistancePosition { monster.position.future };
|
||
|
|
const int distance = playerPosition.ApproxDistance(monsterDistancePosition);
|
||
|
|
if (distance > maxDistance)
|
||
|
|
continue;
|
||
|
|
|
||
|
|
result.push_back(TrackerCandidate {
|
||
|
|
.id = monsterId,
|
||
|
|
.distance = distance,
|
||
|
|
.name = monster.name(),
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
std::sort(result.begin(), result.end(), [](const TrackerCandidate &a, const TrackerCandidate &b) { return IsBetterTrackerCandidate(a, b); });
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
[[nodiscard]] std::optional<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;
|
||
|
|
}
|
||
|
|
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
|
||
|
|
[[nodiscard]] std::optional<int> FindNearestUnopenedChestObjectId(Point playerPosition)
|
||
|
|
{
|
||
|
|
return FindNearestObjectId(playerPosition, IsTrackedChestObject);
|
||
|
|
}
|
||
|
|
|
||
|
|
[[nodiscard]] std::optional<int> FindNearestDoorObjectId(Point playerPosition)
|
||
|
|
{
|
||
|
|
return FindNearestObjectId(playerPosition, IsTrackedDoorObject);
|
||
|
|
}
|
||
|
|
|
||
|
|
[[nodiscard]] std::optional<int> FindNearestShrineObjectId(Point playerPosition)
|
||
|
|
{
|
||
|
|
return FindNearestObjectId(playerPosition, IsShrineLikeObject);
|
||
|
|
}
|
||
|
|
|
||
|
|
[[nodiscard]] std::optional<int> FindNearestBreakableObjectId(Point playerPosition)
|
||
|
|
{
|
||
|
|
return FindNearestObjectId(playerPosition, IsTrackedBreakableObject);
|
||
|
|
}
|
||
|
|
|
||
|
|
[[nodiscard]] std::optional<int> FindNearestMiscInteractableObjectId(Point playerPosition)
|
||
|
|
{
|
||
|
|
return FindNearestObjectId(playerPosition, IsTrackedMiscInteractableObject);
|
||
|
|
}
|
||
|
|
|
||
|
|
[[nodiscard]] 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 (monster.isInvalid)
|
||
|
|
continue;
|
||
|
|
if ((monster.flags & MFLAG_HIDDEN) != 0)
|
||
|
|
continue;
|
||
|
|
if (monster.hitPoints <= 0)
|
||
|
|
continue;
|
||
|
|
|
||
|
|
const Point monsterDistancePosition { monster.position.future };
|
||
|
|
const int distance = playerPosition.ApproxDistance(monsterDistancePosition);
|
||
|
|
if (!bestId || distance < bestDistance) {
|
||
|
|
bestId = monsterId;
|
||
|
|
bestDistance = distance;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return bestId;
|
||
|
|
}
|
||
|
|
|
||
|
|
[[nodiscard]] std::optional<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;
|
||
|
|
}
|
||
|
|
|
||
|
|
} // namespace
|
||
|
|
|
||
|
|
bool PosOkPlayerIgnoreDoors(const Player &player, Point position)
|
||
|
|
{
|
||
|
|
if (!InDungeonBounds(position))
|
||
|
|
return false;
|
||
|
|
if (!IsTileWalkable(position, /*ignoreDoors=*/true))
|
||
|
|
return false;
|
||
|
|
|
||
|
|
Player *otherPlayer = PlayerAtPosition(position);
|
||
|
|
if (otherPlayer != nullptr && otherPlayer != &player && !otherPlayer->hasNoLife())
|
||
|
|
return false;
|
||
|
|
|
||
|
|
if (dMonster[position.x][position.y] != 0) {
|
||
|
|
if (leveltype == DTYPE_TOWN)
|
||
|
|
return false;
|
||
|
|
if (dMonster[position.x][position.y] <= 0)
|
||
|
|
return false;
|
||
|
|
if (!Monsters[dMonster[position.x][position.y] - 1].hasNoLife())
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
namespace {
|
||
|
|
|
||
|
|
[[nodiscard]] bool IsTileWalkableForTrackerPath(Point position, bool ignoreDoors, bool ignoreBreakables)
|
||
|
|
{
|
||
|
|
Object *object = FindObjectAtPosition(position);
|
||
|
|
if (object != nullptr) {
|
||
|
|
if (ignoreDoors && object->isDoor()) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
if (ignoreBreakables && object->_oSolidFlag && object->IsBreakable()) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
if (object->_oSolidFlag) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return IsTileNotSolid(position);
|
||
|
|
}
|
||
|
|
|
||
|
|
} // namespace
|
||
|
|
|
||
|
|
bool PosOkPlayerIgnoreMonsters(const Player &player, Point position)
|
||
|
|
{
|
||
|
|
if (!InDungeonBounds(position))
|
||
|
|
return false;
|
||
|
|
if (!IsTileWalkableForTrackerPath(position, /*ignoreDoors=*/false, /*ignoreBreakables=*/false))
|
||
|
|
return false;
|
||
|
|
|
||
|
|
Player *otherPlayer = PlayerAtPosition(position);
|
||
|
|
if (otherPlayer != nullptr && otherPlayer != &player && !otherPlayer->hasNoLife())
|
||
|
|
return false;
|
||
|
|
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool PosOkPlayerIgnoreDoorsAndMonsters(const Player &player, Point position)
|
||
|
|
{
|
||
|
|
if (!InDungeonBounds(position))
|
||
|
|
return false;
|
||
|
|
if (!IsTileWalkableForTrackerPath(position, /*ignoreDoors=*/true, /*ignoreBreakables=*/false))
|
||
|
|
return false;
|
||
|
|
|
||
|
|
Player *otherPlayer = PlayerAtPosition(position);
|
||
|
|
if (otherPlayer != nullptr && otherPlayer != &player && !otherPlayer->hasNoLife())
|
||
|
|
return false;
|
||
|
|
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool PosOkPlayerIgnoreDoorsMonstersAndBreakables(const Player &player, Point position)
|
||
|
|
{
|
||
|
|
if (!InDungeonBounds(position))
|
||
|
|
return false;
|
||
|
|
if (!IsTileWalkableForTrackerPath(position, /*ignoreDoors=*/true, /*ignoreBreakables=*/true))
|
||
|
|
return false;
|
||
|
|
|
||
|
|
Player *otherPlayer = PlayerAtPosition(position);
|
||
|
|
if (otherPlayer != nullptr && otherPlayer != &player && !otherPlayer->hasNoLife())
|
||
|
|
return false;
|
||
|
|
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
namespace {
|
||
|
|
|
||
|
|
struct DoorBlockInfo {
|
||
|
|
Point beforeDoor;
|
||
|
|
Point doorPosition;
|
||
|
|
};
|
||
|
|
|
||
|
|
std::optional<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->_oSolidFlag) {
|
||
|
|
return DoorBlockInfo { .beforeDoor = position, .doorPosition = next };
|
||
|
|
}
|
||
|
|
position = next;
|
||
|
|
}
|
||
|
|
return std::nullopt;
|
||
|
|
}
|
||
|
|
|
||
|
|
enum class TrackerPathBlockType : uint8_t {
|
||
|
|
Door,
|
||
|
|
Monster,
|
||
|
|
Breakable,
|
||
|
|
};
|
||
|
|
|
||
|
|
struct TrackerPathBlockInfo {
|
||
|
|
TrackerPathBlockType type;
|
||
|
|
size_t stepIndex;
|
||
|
|
Point beforeBlock;
|
||
|
|
Point blockPosition;
|
||
|
|
};
|
||
|
|
|
||
|
|
[[nodiscard]] std::optional<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->_oSolidFlag) {
|
||
|
|
return TrackerPathBlockInfo {
|
||
|
|
.type = TrackerPathBlockType::Door,
|
||
|
|
.stepIndex = i,
|
||
|
|
.beforeBlock = position,
|
||
|
|
.blockPosition = next,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
if (considerBreakables && object != nullptr && object->_oSolidFlag && object->IsBreakable()) {
|
||
|
|
return TrackerPathBlockInfo {
|
||
|
|
.type = TrackerPathBlockType::Breakable,
|
||
|
|
.stepIndex = i,
|
||
|
|
.beforeBlock = position,
|
||
|
|
.blockPosition = next,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
if (considerMonsters && leveltype != DTYPE_TOWN && dMonster[next.x][next.y] != 0) {
|
||
|
|
const int monsterRef = dMonster[next.x][next.y];
|
||
|
|
const int monsterId = std::abs(monsterRef) - 1;
|
||
|
|
const bool blocks = monsterRef <= 0 || (monsterId >= 0 && monsterId < static_cast<int>(MaxMonsters) && !Monsters[monsterId].hasNoLife());
|
||
|
|
if (blocks) {
|
||
|
|
return TrackerPathBlockInfo {
|
||
|
|
.type = TrackerPathBlockType::Monster,
|
||
|
|
.stepIndex = i,
|
||
|
|
.beforeBlock = position,
|
||
|
|
.blockPosition = next,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
position = next;
|
||
|
|
}
|
||
|
|
|
||
|
|
return std::nullopt;
|
||
|
|
}
|
||
|
|
|
||
|
|
static void NavigateToTrackerTargetKeyPressedImpl()
|
||
|
|
{
|
||
|
|
if (!CanPlayerTakeAction() || InGameMenu())
|
||
|
|
return;
|
||
|
|
if (leveltype == DTYPE_TOWN && IsNoneOf(SelectedTrackerTargetCategory, TrackerTargetCategory::Items, TrackerTargetCategory::DeadBodies)) {
|
||
|
|
SpeakText(_("Not in a dungeon."), true);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (AutomapActive) {
|
||
|
|
SpeakText(_("Close the map first."), true);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (MyPlayer == nullptr)
|
||
|
|
return;
|
||
|
|
|
||
|
|
EnsureTrackerLocksMatchCurrentLevel();
|
||
|
|
|
||
|
|
const SDL_Keymod modState = SDL_GetModState();
|
||
|
|
const bool cycleTarget = (modState & SDL_KMOD_SHIFT) != 0;
|
||
|
|
const bool clearTarget = (modState & SDL_KMOD_CTRL) != 0;
|
||
|
|
|
||
|
|
const Point playerPosition = MyPlayer->position.future;
|
||
|
|
AutoWalkTrackerTargetId = -1;
|
||
|
|
|
||
|
|
int &lockedTargetId = LockedTrackerTargetId(SelectedTrackerTargetCategory);
|
||
|
|
if (clearTarget) {
|
||
|
|
lockedTargetId = -1;
|
||
|
|
SpeakText(_("Tracker target cleared."), true);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
std::optional<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 = tracked.name();
|
||
|
|
DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates);
|
||
|
|
if (!cycleTarget) {
|
||
|
|
targetPosition = tracked.position;
|
||
|
|
if (FindObjectAtPosition(tracked.position + Direction::NorthEast) == &tracked)
|
||
|
|
alternateTargetPosition = tracked.position + Direction::NorthEast;
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
case TrackerTargetCategory::Breakables: {
|
||
|
|
const std::vector<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 = tracked.name();
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
|
||
|
|
} // namespace
|
||
|
|
|
||
|
|
void NavigateToTrackerTargetKeyPressed()
|
||
|
|
{
|
||
|
|
NavigateToTrackerTargetKeyPressedImpl();
|
||
|
|
}
|
||
|
|
|
||
|
|
namespace {
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 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) <= TrackerInteractDistanceTiles) {
|
||
|
|
AutoWalkTrackerTargetId = -1;
|
||
|
|
SpeakText(_(inRangeMessage), true);
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
destination = FindBestAdjacentApproachTile(myPlayer, playerPosition, object.position);
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Resolves which object to walk toward for the given tracker category.
|
||
|
|
*/
|
||
|
|
template <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;
|
||
|
|
}
|
||
|
|
// findNearest guarantees the result passes isValid, but verify defensively.
|
||
|
|
if (!isValid(Objects[*targetId])) {
|
||
|
|
SpeakText(_(notFoundMessage), true);
|
||
|
|
return std::nullopt;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
lockedTargetId = *targetId;
|
||
|
|
targetName = getName(*targetId);
|
||
|
|
return targetId;
|
||
|
|
}
|
||
|
|
|
||
|
|
} // namespace
|
||
|
|
|
||
|
|
void CycleTrackerTargetKeyPressed()
|
||
|
|
{
|
||
|
|
if (!CanPlayerTakeAction() || InGameMenu())
|
||
|
|
return;
|
||
|
|
|
||
|
|
AutoWalkTrackerTargetId = -1;
|
||
|
|
|
||
|
|
const SDL_Keymod modState = SDL_GetModState();
|
||
|
|
const bool cyclePrevious = (modState & SDL_KMOD_SHIFT) != 0;
|
||
|
|
|
||
|
|
if (cyclePrevious) {
|
||
|
|
switch (SelectedTrackerTargetCategory) {
|
||
|
|
case TrackerTargetCategory::Items:
|
||
|
|
SelectedTrackerTargetCategory = TrackerTargetCategory::DeadBodies;
|
||
|
|
break;
|
||
|
|
case TrackerTargetCategory::Chests:
|
||
|
|
SelectedTrackerTargetCategory = TrackerTargetCategory::Items;
|
||
|
|
break;
|
||
|
|
case TrackerTargetCategory::Doors:
|
||
|
|
SelectedTrackerTargetCategory = TrackerTargetCategory::Chests;
|
||
|
|
break;
|
||
|
|
case TrackerTargetCategory::Shrines:
|
||
|
|
SelectedTrackerTargetCategory = TrackerTargetCategory::Doors;
|
||
|
|
break;
|
||
|
|
case TrackerTargetCategory::Objects:
|
||
|
|
SelectedTrackerTargetCategory = TrackerTargetCategory::Shrines;
|
||
|
|
break;
|
||
|
|
case TrackerTargetCategory::Breakables:
|
||
|
|
SelectedTrackerTargetCategory = TrackerTargetCategory::Objects;
|
||
|
|
break;
|
||
|
|
case TrackerTargetCategory::Monsters:
|
||
|
|
SelectedTrackerTargetCategory = TrackerTargetCategory::Breakables;
|
||
|
|
break;
|
||
|
|
case TrackerTargetCategory::DeadBodies:
|
||
|
|
default:
|
||
|
|
SelectedTrackerTargetCategory = TrackerTargetCategory::Monsters;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
switch (SelectedTrackerTargetCategory) {
|
||
|
|
case TrackerTargetCategory::Items:
|
||
|
|
SelectedTrackerTargetCategory = TrackerTargetCategory::Chests;
|
||
|
|
break;
|
||
|
|
case TrackerTargetCategory::Chests:
|
||
|
|
SelectedTrackerTargetCategory = TrackerTargetCategory::Doors;
|
||
|
|
break;
|
||
|
|
case TrackerTargetCategory::Doors:
|
||
|
|
SelectedTrackerTargetCategory = TrackerTargetCategory::Shrines;
|
||
|
|
break;
|
||
|
|
case TrackerTargetCategory::Shrines:
|
||
|
|
SelectedTrackerTargetCategory = TrackerTargetCategory::Objects;
|
||
|
|
break;
|
||
|
|
case TrackerTargetCategory::Objects:
|
||
|
|
SelectedTrackerTargetCategory = TrackerTargetCategory::Breakables;
|
||
|
|
break;
|
||
|
|
case TrackerTargetCategory::Breakables:
|
||
|
|
SelectedTrackerTargetCategory = TrackerTargetCategory::Monsters;
|
||
|
|
break;
|
||
|
|
case TrackerTargetCategory::Monsters:
|
||
|
|
SelectedTrackerTargetCategory = TrackerTargetCategory::DeadBodies;
|
||
|
|
break;
|
||
|
|
case TrackerTargetCategory::DeadBodies:
|
||
|
|
default:
|
||
|
|
SelectedTrackerTargetCategory = TrackerTargetCategory::Items;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
SpeakTrackerTargetCategory();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Called each game tick to advance auto-walk toward the current tracker target.
|
||
|
|
*/
|
||
|
|
void UpdateAutoWalkTracker()
|
||
|
|
{
|
||
|
|
if (AutoWalkTrackerTargetId < 0)
|
||
|
|
return;
|
||
|
|
if (leveltype == DTYPE_TOWN || IsPlayerInStore() || ChatLogFlag || HelpFlag || InGameMenu()) {
|
||
|
|
AutoWalkTrackerTargetId = -1;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (!CanPlayerTakeAction())
|
||
|
|
return;
|
||
|
|
|
||
|
|
if (MyPlayer == nullptr) {
|
||
|
|
SpeakText(_("Cannot walk right now."), true);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (MyPlayer->_pmode != PM_STAND)
|
||
|
|
return;
|
||
|
|
if (MyPlayer->walkpath[0] != WALK_NONE)
|
||
|
|
return;
|
||
|
|
if (MyPlayer->destAction != ACTION_NONE)
|
||
|
|
return;
|
||
|
|
|
||
|
|
Player &myPlayer = *MyPlayer;
|
||
|
|
const Point playerPosition = myPlayer.position.future;
|
||
|
|
|
||
|
|
std::optional<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;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
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 no direct path exists, try pathfinding that treats closed doors as walkable.
|
||
|
|
// If that finds a path, identify the first closed door along it and re-route the
|
||
|
|
// player to the tile just before that door, so they can open it and retry.
|
||
|
|
if (steps == 0) {
|
||
|
|
std::array<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;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// FindPath returns 0 if the path length is equal to the maximum.
|
||
|
|
// The player walkpath buffer is MaxPathLengthPlayer, so keep segments strictly shorter.
|
||
|
|
if (steps < static_cast<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);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Initiates auto-walk toward the currently selected tracker target.
|
||
|
|
*/
|
||
|
|
void AutoWalkToTrackerTargetKeyPressed()
|
||
|
|
{
|
||
|
|
// Cancel in-progress auto-walk (must be checked before the action guard
|
||
|
|
// so that cancellation works even if the player is mid-walk).
|
||
|
|
if (AutoWalkTrackerTargetId >= 0) {
|
||
|
|
CancelAutoWalk();
|
||
|
|
SpeakText(_("Walk cancelled."), true);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Defense-in-depth: keymapper canTrigger also checks these, but guard here
|
||
|
|
// in case the function is called from another code path.
|
||
|
|
if (!CanPlayerTakeAction() || InGameMenu())
|
||
|
|
return;
|
||
|
|
|
||
|
|
if (leveltype == DTYPE_TOWN) {
|
||
|
|
SpeakText(_("Not in a dungeon."), true);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (AutomapActive) {
|
||
|
|
SpeakText(_("Close the map first."), true);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
if (MyPlayer == nullptr) {
|
||
|
|
SpeakText(_("Cannot walk right now."), true);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
EnsureTrackerLocksMatchCurrentLevel();
|
||
|
|
|
||
|
|
const Point playerPosition = MyPlayer->position.future;
|
||
|
|
int &lockedTargetId = LockedTrackerTargetId(SelectedTrackerTargetCategory);
|
||
|
|
|
||
|
|
std::optional<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 Objects[id].name(); },
|
||
|
|
N_("No objects found."), targetName);
|
||
|
|
if (!targetId)
|
||
|
|
return;
|
||
|
|
break;
|
||
|
|
case TrackerTargetCategory::Breakables:
|
||
|
|
targetId = ResolveObjectTrackerTarget(lockedTargetId, playerPosition,
|
||
|
|
IsTrackedBreakableObject, FindNearestBreakableObjectId,
|
||
|
|
[](int id) -> StringOrView { return Objects[id].name(); },
|
||
|
|
N_("No breakables found."), targetName);
|
||
|
|
if (!targetId)
|
||
|
|
return;
|
||
|
|
break;
|
||
|
|
case TrackerTargetCategory::Monsters: {
|
||
|
|
if (lockedTargetId >= 0 && lockedTargetId < static_cast<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;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string msg;
|
||
|
|
StrAppend(msg, _("Going to: "), targetName);
|
||
|
|
SpeakText(msg, true);
|
||
|
|
|
||
|
|
AutoWalkTrackerTargetId = *targetId;
|
||
|
|
AutoWalkTrackerTargetCategory = SelectedTrackerTargetCategory;
|
||
|
|
UpdateAutoWalkTracker();
|
||
|
|
}
|
||
|
|
|
||
|
|
} // namespace devilution
|