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.
 
 
 
 
 
 

358 lines
9.4 KiB

#include "accessibility/town_navigation.hpp"
#include <algorithm>
#include <array>
#include <cstddef>
#include <string>
#include <vector>
#include <fmt/format.h>
#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<int> 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<int>(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<int>(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<int>(it - TownNpcOrder.begin()) : 0;
const int size = static_cast<int>(TownNpcOrder.size());
int newIndex = (currentIndex + delta) % size;
if (newIndex < 0)
newIndex += size;
SelectedTownNpc = TownNpcOrder[static_cast<size_t>(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<int>(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<int>(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<int>(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<uint16_t>(townerIdx));
return;
}
constexpr size_t MaxAutoWalkPathLength = 512;
std::array<int8_t, MaxAutoWalkPathLength> 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<int>(MaxPathLengthPlayer)) {
const int townerIdx = AutoWalkTownNpcTarget;
AutoWalkTownNpcTarget = -1;
NetSendCmdLocParam1(true, CMD_TALKXY, towner.position, static_cast<uint16_t>(townerIdx));
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 ListTownNpcsKeyPressed()
{
if (leveltype != DTYPE_TOWN) {
ResetTownNpcSelection();
SpeakText(_("Not in town."), true);
return;
}
if (IsPlayerInStore())
return;
std::vector<const Towner *> townNpcs;
std::vector<const Towner *> 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<int>(cows.size()));
}
RefreshTownNpcOrder(true);
if (SelectedTownNpc >= 0 && SelectedTownNpc < static_cast<int>(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