#include "accessibility/town_navigation.hpp" #include #include #include #include #include #include #include "controls/plrctrls.h" #include "diablo.h" #include "engine/path.h" #include "help.h" #include "levels/gendung.h" #include "levels/tile_properties.hpp" #include "multi.h" #include "options.h" #include "player.h" #include "qol/chatlog.h" #include "stores.h" #include "towners.h" #include "utils/language.h" #include "utils/screen_reader.hpp" #include "utils/str_cat.hpp" namespace devilution { namespace { std::vector TownNpcOrder; int SelectedTownNpc = -1; int AutoWalkTownNpcTarget = -1; void ResetTownNpcSelection() { TownNpcOrder.clear(); SelectedTownNpc = -1; } void RefreshTownNpcOrder(bool selectFirst = false) { TownNpcOrder.clear(); if (leveltype != DTYPE_TOWN) return; const Point playerPosition = MyPlayer->position.future; for (size_t i = 0; i < GetNumTowners(); ++i) { const Towner &towner = Towners[i]; if (!IsTownerPresent(towner._ttype)) continue; if (towner._ttype == TOWN_COW) continue; TownNpcOrder.push_back(static_cast(i)); } if (TownNpcOrder.empty()) { SelectedTownNpc = -1; return; } std::sort(TownNpcOrder.begin(), TownNpcOrder.end(), [&playerPosition](int a, int b) { const Towner &townerA = Towners[a]; const Towner &townerB = Towners[b]; const int distanceA = playerPosition.WalkingDistance(townerA.position); const int distanceB = playerPosition.WalkingDistance(townerB.position); if (distanceA != distanceB) return distanceA < distanceB; return townerA.name < townerB.name; }); if (selectFirst) { SelectedTownNpc = TownNpcOrder.front(); return; } const auto it = std::find(TownNpcOrder.begin(), TownNpcOrder.end(), SelectedTownNpc); if (it == TownNpcOrder.end()) SelectedTownNpc = TownNpcOrder.front(); } void EnsureTownNpcOrder() { if (leveltype != DTYPE_TOWN) { ResetTownNpcSelection(); return; } if (TownNpcOrder.empty()) { RefreshTownNpcOrder(true); return; } if (SelectedTownNpc < 0 || SelectedTownNpc >= static_cast(GetNumTowners())) { RefreshTownNpcOrder(true); return; } const auto it = std::find(TownNpcOrder.begin(), TownNpcOrder.end(), SelectedTownNpc); if (it == TownNpcOrder.end()) SelectedTownNpc = TownNpcOrder.front(); } void SelectTownNpcRelative(int delta) { if (!IsTownNpcActionAllowed()) return; EnsureTownNpcOrder(); if (TownNpcOrder.empty()) { SpeakText(_("No town NPCs found."), true); return; } auto it = std::find(TownNpcOrder.begin(), TownNpcOrder.end(), SelectedTownNpc); int currentIndex = (it != TownNpcOrder.end()) ? static_cast(it - TownNpcOrder.begin()) : 0; const int size = static_cast(TownNpcOrder.size()); int newIndex = (currentIndex + delta) % size; if (newIndex < 0) newIndex += size; SelectedTownNpc = TownNpcOrder[static_cast(newIndex)]; SpeakSelectedTownNpc(); } 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; } } // namespace bool IsTownNpcActionAllowed() { return CanPlayerTakeAction() && leveltype == DTYPE_TOWN && !IsPlayerInStore() && !ChatLogFlag && !HelpFlag; } void SpeakSelectedTownNpc() { EnsureTownNpcOrder(); if (SelectedTownNpc < 0 || SelectedTownNpc >= static_cast(GetNumTowners())) { SpeakText(_("No NPC selected."), true); return; } const Towner &towner = Towners[SelectedTownNpc]; const Point playerPosition = MyPlayer->position.future; const int distance = playerPosition.WalkingDistance(towner.position); std::string msg; StrAppend(msg, towner.name); StrAppend(msg, "\n", _("Distance: "), distance); StrAppend(msg, "\n", _("Position: "), towner.position.x, ", ", towner.position.y); SpeakText(msg, true); } void CancelTownNpcAutoWalk() { AutoWalkTownNpcTarget = -1; } void SelectNextTownNpcKeyPressed() { SelectTownNpcRelative(+1); } void SelectPreviousTownNpcKeyPressed() { SelectTownNpcRelative(-1); } void GoToSelectedTownNpcKeyPressed() { if (!IsTownNpcActionAllowed()) return; EnsureTownNpcOrder(); if (SelectedTownNpc < 0 || SelectedTownNpc >= static_cast(GetNumTowners())) { SpeakText(_("No NPC selected."), true); return; } const Towner &towner = Towners[SelectedTownNpc]; std::string msg; StrAppend(msg, _("Going to: "), towner.name); SpeakText(msg, true); AutoWalkTownNpcTarget = SelectedTownNpc; UpdateAutoWalkTownNpc(); } void UpdateAutoWalkTownNpc() { if (AutoWalkTownNpcTarget < 0) return; if (leveltype != DTYPE_TOWN || IsPlayerInStore() || ChatLogFlag || HelpFlag) { AutoWalkTownNpcTarget = -1; return; } if (!CanPlayerTakeAction()) return; if (MyPlayer->_pmode != PM_STAND) return; if (MyPlayer->walkpath[0] != WALK_NONE) return; if (MyPlayer->destAction != ACTION_NONE) return; if (AutoWalkTownNpcTarget >= static_cast(GetNumTowners())) { AutoWalkTownNpcTarget = -1; SpeakText(_("No NPC selected."), true); return; } const Towner &towner = Towners[AutoWalkTownNpcTarget]; if (!IsTownerPresent(towner._ttype) || towner._ttype == TOWN_COW) { AutoWalkTownNpcTarget = -1; SpeakText(_("No NPC selected."), true); return; } Player &myPlayer = *MyPlayer; const Point playerPosition = myPlayer.position.future; if (playerPosition.WalkingDistance(towner.position) < 2) { const int townerIdx = AutoWalkTownNpcTarget; AutoWalkTownNpcTarget = -1; NetSendCmdLocParam1(true, CMD_TALKXY, towner.position, static_cast(townerIdx)); return; } constexpr size_t MaxAutoWalkPathLength = 512; std::array path; path.fill(WALK_NONE); const int steps = FindPath(CanStep, [&myPlayer](Point position) { return PosOkPlayer(myPlayer, position); }, playerPosition, towner.position, path.data(), path.size()); if (steps == 0) { AutoWalkTownNpcTarget = -1; std::string error; StrAppend(error, _("Can't find a path to: "), towner.name); SpeakText(error, 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(MaxPathLengthPlayer)) { const int townerIdx = AutoWalkTownNpcTarget; AutoWalkTownNpcTarget = -1; NetSendCmdLocParam1(true, CMD_TALKXY, towner.position, static_cast(townerIdx)); return; } const int segmentSteps = std::min(steps - 1, static_cast(MaxPathLengthPlayer - 1)); const Point waypoint = PositionAfterWalkPathSteps(playerPosition, path.data(), segmentSteps); NetSendCmdLoc(MyPlayerId, true, CMD_WALKXY, waypoint); } void ListTownNpcsKeyPressed() { if (leveltype != DTYPE_TOWN) { ResetTownNpcSelection(); SpeakText(_("Not in town."), true); return; } if (IsPlayerInStore()) return; std::vector townNpcs; std::vector cows; townNpcs.reserve(Towners.size()); cows.reserve(Towners.size()); const Point playerPosition = MyPlayer->position.future; for (const Towner &towner : Towners) { if (!IsTownerPresent(towner._ttype)) continue; if (towner._ttype == TOWN_COW) { cows.push_back(&towner); continue; } townNpcs.push_back(&towner); } if (townNpcs.empty() && cows.empty()) { ResetTownNpcSelection(); SpeakText(_("No town NPCs found."), true); return; } std::sort(townNpcs.begin(), townNpcs.end(), [&playerPosition](const Towner *a, const Towner *b) { const int distanceA = playerPosition.WalkingDistance(a->position); const int distanceB = playerPosition.WalkingDistance(b->position); if (distanceA != distanceB) return distanceA < distanceB; return a->name < b->name; }); std::string output; StrAppend(output, _("Town NPCs:")); for (size_t i = 0; i < townNpcs.size(); ++i) { StrAppend(output, "\n", i + 1, ". ", townNpcs[i]->name); } if (!cows.empty()) { StrAppend(output, "\n", _("Cows: "), static_cast(cows.size())); } RefreshTownNpcOrder(true); if (SelectedTownNpc >= 0 && SelectedTownNpc < static_cast(GetNumTowners())) { const Towner &towner = Towners[SelectedTownNpc]; StrAppend(output, "\n", _("Selected: "), towner.name); StrAppend(output, "\n", _("PageUp/PageDown: select. Home: go. End: repeat.")); } const std::string_view exitKey = GetOptions().Keymapper.KeyNameForAction("SpeakNearestExit"); if (!exitKey.empty()) { StrAppend(output, "\n", fmt::format(fmt::runtime(_("Cathedral entrance: press {:s}.")), exitKey)); } SpeakText(output, true); } } // namespace devilution