diff --git a/README.md b/README.md index 6f0cb8f97..bbe8fd41f 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ Keybinds are configurable, but these are the defaults most players will use: - `Ctrl`+`N` - clear the tracker target. - `H` - speak nearest unexplored space. - `E` - speak nearest exit (hold `Shift` for quest entrances; in town: `Ctrl`+`E` cycles dungeon entrances). +- `P` - speak nearest open town portal (town only). - `,` - speak nearest stairs up. - `.` - speak nearest stairs down. - `L` - speak current dungeon + floor. diff --git a/Source/diablo.cpp b/Source/diablo.cpp index 653be4a90..aad60ae87 100644 --- a/Source/diablo.cpp +++ b/Source/diablo.cpp @@ -4542,6 +4542,93 @@ std::optional FindNearestTownPortalOnCurrentLevel() return bestPosition; } +struct TownPortalInTown { + int portalIndex; + Point position; + int distance; +}; + +std::optional FindNearestTownPortalInTown() +{ + if (MyPlayer == nullptr || leveltype != DTYPE_TOWN) + return std::nullopt; + + const Point playerPosition = MyPlayer->position.future; + + std::optional best; + int bestDistance = 0; + + for (const Missile &missile : Missiles) { + if (missile._mitype != MissileID::TownPortal) + continue; + if (missile._misource < 0 || missile._misource >= MAXPORTAL) + continue; + if (!Portals[missile._misource].open) + continue; + + const Point portalPosition = missile.position.tile; + const int distance = playerPosition.WalkingDistance(portalPosition); + if (!best || distance < bestDistance) { + best = TownPortalInTown { + .portalIndex = missile._misource, + .position = portalPosition, + .distance = distance, + }; + bestDistance = distance; + } + } + + return best; +} + +[[nodiscard]] std::string TownPortalLabelForSpeech(const Portal &portal) +{ + if (portal.level <= 0) + return std::string { _("Town portal") }; + + if (portal.setlvl) { + const auto questLevel = static_cast<_setlevels>(portal.level); + const char *questLevelName = QuestLevelNames[questLevel]; + if (questLevelName == nullptr || questLevelName[0] == '\0') + return std::string { _("Town portal to set level") }; + + return fmt::format(fmt::runtime(_(/* TRANSLATORS: {:s} is a set/quest level name. */ "Town portal to {:s}")), _(questLevelName)); + } + + constexpr std::array DungeonStrs = { + N_("Town"), + N_("Cathedral"), + N_("Catacombs"), + N_("Caves"), + N_("Hell"), + N_("Nest"), + N_("Crypt"), + }; + std::string dungeonStr; + if (portal.ltype >= DTYPE_TOWN && portal.ltype <= DTYPE_LAST) { + dungeonStr = _(DungeonStrs[static_cast(portal.ltype)]); + } else { + dungeonStr = _(/* TRANSLATORS: type of dungeon (i.e. Cathedral, Caves)*/ "None"); + } + + int floor = portal.level; + if (portal.ltype == DTYPE_CATACOMBS) + floor -= 4; + else if (portal.ltype == DTYPE_CAVES) + floor -= 8; + else if (portal.ltype == DTYPE_HELL) + floor -= 12; + else if (portal.ltype == DTYPE_NEST) + floor -= 16; + else if (portal.ltype == DTYPE_CRYPT) + floor -= 20; + + if (floor > 0) + return fmt::format(fmt::runtime(_(/* TRANSLATORS: {:s} is a dungeon name and {:d} is a floor number. */ "Town portal to {:s} {:d}")), dungeonStr, floor); + + return fmt::format(fmt::runtime(_(/* TRANSLATORS: {:s} is a dungeon name. */ "Town portal to {:s}")), dungeonStr); +} + struct QuestSetLevelEntrance { _setlevels questLevel; Point entrancePosition; @@ -4718,6 +4805,42 @@ void SpeakNearestExitKeyPressed() SpeakText(message, true); } +void SpeakNearestTownPortalInTownKeyPressed() +{ + if (!CanPlayerTakeAction()) + return; + if (AutomapActive) { + SpeakText(_("Close the map first."), true); + return; + } + if (leveltype != DTYPE_TOWN) { + SpeakText(_("Not in town."), true); + return; + } + if (MyPlayer == nullptr) + return; + + const std::optional portal = FindNearestTownPortalInTown(); + if (!portal) { + SpeakText(_("No town portals found."), true); + return; + } + + const Point startPosition = MyPlayer->position.future; + const Point targetPosition = portal->position; + + const std::optional> path = FindKeyboardWalkPathForSpeech(*MyPlayer, startPosition, targetPosition); + + std::string message = TownPortalLabelForSpeech(Portals[portal->portalIndex]); + message.append(": "); + if (!path) + AppendDirectionalFallback(message, targetPosition - startPosition); + else + AppendKeyboardWalkPathForSpeech(message, *path); + + SpeakText(message, true); +} + void SpeakNearestStairsKeyPressed(int triggerMessage) { if (!CanPlayerTakeAction()) @@ -5525,8 +5648,8 @@ void InitKeymapActions() "PauseGame", N_("Pause Game"), N_("Pauses the game."), - 'P', - diablo_pause_game); + SDLK_UNKNOWN, + diablo_pause_game); options.Keymapper.AddAction( "PauseGameAlternate", N_("Pause Game (Alternate)"), @@ -5598,7 +5721,7 @@ void InitKeymapActions() "SortInv", N_("Sort Inventory"), N_("Sorts the inventory."), - 'R', + 'R', [] { ReorganizeInventory(*MyPlayer); }); @@ -5615,11 +5738,19 @@ void InitKeymapActions() "Programming is like magic.", 'X', [] { - DebugToggle = !DebugToggle; - }); -#endif - options.Keymapper.CommitActions(); -} + DebugToggle = !DebugToggle; + }); +#endif + options.Keymapper.AddAction( + "SpeakNearestTownPortal", + N_("Nearest town portal"), + N_("Speaks directions to the nearest open town portal in town."), + 'P', + SpeakNearestTownPortalInTownKeyPressed, + nullptr, + []() { return CanPlayerTakeAction() && leveltype == DTYPE_TOWN; }); + options.Keymapper.CommitActions(); +} void InitPadmapActions() {