|
|
|
|
@ -280,17 +280,51 @@ struct TrackerCandidate {
|
|
|
|
|
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); |
|
|
|
|
[[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); |
|
|
|
|
@ -316,14 +350,14 @@ struct TrackerCandidate {
|
|
|
|
|
if (distance > maxDistance) |
|
|
|
|
continue; |
|
|
|
|
|
|
|
|
|
result.push_back(TrackerCandidate { |
|
|
|
|
.id = itemId, |
|
|
|
|
.distance = distance, |
|
|
|
|
.name = item.getName(), |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
result.push_back(TrackerCandidate { |
|
|
|
|
.id = itemId, |
|
|
|
|
.distance = distance, |
|
|
|
|
.name = ItemLabelForSpeech(item), |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
std::sort(result.begin(), result.end(), IsBetterTrackerCandidate); |
|
|
|
|
return result; |
|
|
|
|
} |
|
|
|
|
@ -644,11 +678,11 @@ template <typename Predicate>
|
|
|
|
|
return result; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
for (int i = 0; i < numtrigs; ++i) { |
|
|
|
|
const TriggerStruct &trigger = trigs[i]; |
|
|
|
|
if (setlevel) { |
|
|
|
|
if (trigger._tmsg != WM_DIABRTNLVL) |
|
|
|
|
continue; |
|
|
|
|
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; |
|
|
|
|
@ -659,13 +693,31 @@ template <typename Predicate>
|
|
|
|
|
result.push_back(TrackerCandidate { |
|
|
|
|
.id = i, |
|
|
|
|
.distance = distance, |
|
|
|
|
.name = TriggerLabelForSpeech(trigger), |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
std::sort(result.begin(), result.end(), IsBetterTrackerCandidate); |
|
|
|
|
return result; |
|
|
|
|
} |
|
|
|
|
.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) |
|
|
|
|
{ |
|
|
|
|
@ -774,24 +826,39 @@ template <typename Predicate>
|
|
|
|
|
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; |
|
|
|
|
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), |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
std::sort(result.begin(), result.end(), IsBetterTrackerCandidate); |
|
|
|
|
return result; |
|
|
|
|
} |
|
|
|
|
.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); |
|
|
|
|
@ -1977,10 +2044,10 @@ void NavigateToTrackerTargetKeyPressed()
|
|
|
|
|
} |
|
|
|
|
break; |
|
|
|
|
} |
|
|
|
|
case TrackerTargetCategory::DungeonEntrances: { |
|
|
|
|
const std::vector<TrackerCandidate> nearbyCandidates = CollectDungeonEntranceTrackerCandidates(playerPosition); |
|
|
|
|
if (cycleTarget) { |
|
|
|
|
targetId = FindNextTrackerCandidateId(nearbyCandidates, lockedTargetId); |
|
|
|
|
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); |
|
|
|
|
@ -1988,32 +2055,35 @@ void NavigateToTrackerTargetKeyPressed()
|
|
|
|
|
SpeakText(_("No next dungeon entrance."), true); |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
} else if (lockedTargetId >= 0 && lockedTargetId < numtrigs) { |
|
|
|
|
targetId = lockedTargetId; |
|
|
|
|
} else if (!nearbyCandidates.empty()) { |
|
|
|
|
targetId = nearbyCandidates.front().id; |
|
|
|
|
} |
|
|
|
|
if (!targetId) { |
|
|
|
|
SpeakText(_("No dungeon entrances found."), 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 = TriggerLabelForSpeech(trigs[*targetId]); |
|
|
|
|
DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates); |
|
|
|
|
if (!cycleTarget) { |
|
|
|
|
const TriggerStruct &trigger = trigs[*targetId]; |
|
|
|
|
targetPosition = Point { trigger.position.x, trigger.position.y }; |
|
|
|
|
} |
|
|
|
|
break; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
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) { |
|
|
|
|
@ -2051,10 +2121,10 @@ void NavigateToTrackerTargetKeyPressed()
|
|
|
|
|
} |
|
|
|
|
break; |
|
|
|
|
} |
|
|
|
|
case TrackerTargetCategory::QuestLocations: { |
|
|
|
|
const std::vector<TrackerCandidate> nearbyCandidates = CollectQuestLocationTrackerCandidates(playerPosition); |
|
|
|
|
if (cycleTarget) { |
|
|
|
|
targetId = FindNextTrackerCandidateId(nearbyCandidates, lockedTargetId); |
|
|
|
|
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); |
|
|
|
|
@ -2062,14 +2132,13 @@ void NavigateToTrackerTargetKeyPressed()
|
|
|
|
|
SpeakText(_("No next quest location."), true); |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
} else if ((setlevel && lockedTargetId >= 0 && lockedTargetId < numtrigs) || (!setlevel && lockedTargetId >= 0 && lockedTargetId < static_cast<int>(sizeof(Quests) / sizeof(Quests[0])))) { |
|
|
|
|
targetId = lockedTargetId; |
|
|
|
|
} else if (!nearbyCandidates.empty()) { |
|
|
|
|
targetId = nearbyCandidates.front().id; |
|
|
|
|
} |
|
|
|
|
if (!targetId) { |
|
|
|
|
SpeakText(_("No quest locations found."), 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; }); |
|
|
|
|
@ -2081,15 +2150,19 @@ void NavigateToTrackerTargetKeyPressed()
|
|
|
|
|
|
|
|
|
|
lockedTargetId = *targetId; |
|
|
|
|
targetName = std::string(it->name.str()); |
|
|
|
|
DecorateTrackerTargetNameWithOrdinalIfNeeded(*targetId, targetName, nearbyCandidates); |
|
|
|
|
if (!cycleTarget) { |
|
|
|
|
if (setlevel) { |
|
|
|
|
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; |
|
|
|
|
} |
|
|
|
|
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; |
|
|
|
|
} |
|
|
|
|
|