diff --git a/Source/controls/accessibility_keys.cpp b/Source/controls/accessibility_keys.cpp index faee3177e..2e2290cd9 100644 --- a/Source/controls/accessibility_keys.cpp +++ b/Source/controls/accessibility_keys.cpp @@ -258,16 +258,20 @@ void QuestLogKeyPressed() CloseStash(); } -void SpeakSelectedSpeedbookSpell() -{ - for (const auto &spellListItem : GetSpellListItems()) { - if (spellListItem.isSelected) { - SpeakText(pgettext("spell", GetSpellData(spellListItem.id).sNameText), /*force=*/true); - return; - } - } - SpeakText(_("No spell selected."), /*force=*/true); -} +void SpeakSelectedSpeedbookSpell() +{ + if (MyPlayer == nullptr) + return; + + const Player &player = *MyPlayer; + for (const auto &spellListItem : GetSpellListItems()) { + if (spellListItem.isSelected) { + SpeakText(BuildSpellDetailsForSpeech(player, spellListItem.id, spellListItem.type), /*force=*/true); + return; + } + } + SpeakText(_("No spell selected."), /*force=*/true); +} void DisplaySpellsKeyPressed() { @@ -290,15 +294,15 @@ void SpellBookKeyPressed() { if (IsPlayerInStore()) return; - SpellbookFlag = !SpellbookFlag; - if (SpellbookFlag && MyPlayer != nullptr) { - const Player &player = *MyPlayer; - if (IsValidSpell(player._pRSpell)) { - SpeakText(pgettext("spell", GetSpellData(player._pRSpell).sNameText), /*force=*/true); - } else { - SpeakText(_("No spell selected."), /*force=*/true); - } - } + SpellbookFlag = !SpellbookFlag; + if (SpellbookFlag && MyPlayer != nullptr) { + const Player &player = *MyPlayer; + if (IsValidSpell(player._pRSpell)) { + SpeakText(BuildSpellDetailsForSpeech(player, player._pRSpell, player._pRSplType), /*force=*/true); + } else { + SpeakText(_("No spell selected."), /*force=*/true); + } + } if (!IsLeftPanelOpen() && CanPanelsCoverView()) { if (!SpellbookFlag) { // We closed the spellbook if (MousePosition.x < 480 && MousePosition.y < GetMainPanel().position.y) { diff --git a/Source/controls/plrctrls.cpp b/Source/controls/plrctrls.cpp index 8d9f58384..f05f1e2a6 100644 --- a/Source/controls/plrctrls.cpp +++ b/Source/controls/plrctrls.cpp @@ -850,21 +850,26 @@ void SpeakStashSlotForAccessibility() return; } + const int row = ActiveStashSlot.y + 1; + const int column = ActiveStashSlot.x + 1; + const int cell = ActiveStashSlot.y * StashGridSize.width + ActiveStashSlot.x + 1; + const std::string positionInfo = fmt::format("Row {}, Column {}, Cell {}: ", row, column, cell); + const StashStruct::StashCell itemId = Stash.GetItemIdAtPosition(ActiveStashSlot); if (itemId != StashStruct::EmptyCell) { const Item &item = Stash.stashList[itemId]; if (!item.isEmpty()) { if (item._itype == ItemType::Gold) { const int nGold = item._ivalue; - SpeakText(fmt::format(fmt::runtime(ngettext("{:s} gold piece", "{:s} gold pieces", nGold)), FormatInteger(nGold)), /*force=*/true); + SpeakText(StrCat(positionInfo, fmt::format(fmt::runtime(ngettext("{:s} gold piece", "{:s} gold pieces", nGold)), FormatInteger(nGold))), /*force=*/true); } else { - SpeakText(item.getName(), /*force=*/true); + SpeakText(StrCat(positionInfo, item.getName()), /*force=*/true); } return; } } - SpeakText(_("empty"), /*force=*/true); + SpeakText(StrCat(positionInfo, _("empty")), /*force=*/true); } /** diff --git a/Source/controls/tracker.cpp b/Source/controls/tracker.cpp index 96f9778e7..38cb8e52c 100644 --- a/Source/controls/tracker.cpp +++ b/Source/controls/tracker.cpp @@ -454,12 +454,13 @@ struct TrackerCandidate { return true; } -[[nodiscard]] bool IsTrackedMonster(const Monster &monster) -{ - return !monster.isInvalid - && (monster.flags & MFLAG_HIDDEN) == 0 - && monster.hitPoints > 0; -} +[[nodiscard]] bool IsTrackedMonster(const Monster &monster) +{ + return !monster.isInvalid + && (monster.flags & MFLAG_HIDDEN) == 0 + && monster.hitPoints > 0 + && !(monster.type().type == MT_GOLEM && monster.position.tile == GolemHoldingCell); +} template [[nodiscard]] std::vector CollectNearbyObjectTrackerCandidates(Point playerPosition, int maxDistance, Predicate predicate) diff --git a/Source/diablo.cpp b/Source/diablo.cpp index 43f36bf94..09bf4b0d7 100644 --- a/Source/diablo.cpp +++ b/Source/diablo.cpp @@ -621,9 +621,7 @@ void PressKey(SDL_Keycode vkey, uint16_t modState) } else if (SpellbookFlag && MyPlayer != nullptr && !IsInspectingPlayer()) { const Player &player = *MyPlayer; if (IsValidSpell(player._pRSpell)) { - std::string msg; - StrAppend(msg, _("Selected: "), pgettext("spell", GetSpellData(player._pRSpell).sNameText)); - SpeakText(msg, /*force=*/true); + SpeakText(StrCat(_("Selected: "), BuildSpellDetailsForSpeech(player, player._pRSpell, player._pRSplType)), /*force=*/true); } else { SpeakText(_("No spell selected."), /*force=*/true); } @@ -655,7 +653,7 @@ void PressKey(SDL_Keycode vkey, uint16_t modState) : SpellType::Spell; UpdateSpellTarget(*next); RedrawEverything(); - SpeakText(pgettext("spell", GetSpellData(*next).sNameText), /*force=*/true); + SpeakText(BuildSpellDetailsForSpeech(*MyPlayer, *next, MyPlayer->_pRSplType), /*force=*/true); } } else if (invflag) { InventoryMoveFromKeyboard({ AxisDirectionX_NONE, AxisDirectionY_UP }); @@ -688,7 +686,7 @@ void PressKey(SDL_Keycode vkey, uint16_t modState) : SpellType::Spell; UpdateSpellTarget(*next); RedrawEverything(); - SpeakText(pgettext("spell", GetSpellData(*next).sNameText), /*force=*/true); + SpeakText(BuildSpellDetailsForSpeech(*MyPlayer, *next, MyPlayer->_pRSplType), /*force=*/true); } } else if (invflag) { InventoryMoveFromKeyboard({ AxisDirectionX_NONE, AxisDirectionY_DOWN }); @@ -729,7 +727,7 @@ void PressKey(SDL_Keycode vkey, uint16_t modState) : SpellType::Spell; UpdateSpellTarget(*first); RedrawEverything(); - SpeakText(pgettext("spell", GetSpellData(*first).sNameText), /*force=*/true); + SpeakText(BuildSpellDetailsForSpeech(*MyPlayer, *first, MyPlayer->_pRSplType), /*force=*/true); } } } else if (invflag) { @@ -756,7 +754,7 @@ void PressKey(SDL_Keycode vkey, uint16_t modState) : SpellType::Spell; UpdateSpellTarget(*first); RedrawEverything(); - SpeakText(pgettext("spell", GetSpellData(*first).sNameText), /*force=*/true); + SpeakText(BuildSpellDetailsForSpeech(*MyPlayer, *first, MyPlayer->_pRSplType), /*force=*/true); } } } else if (invflag) { diff --git a/Source/panels/spell_list.cpp b/Source/panels/spell_list.cpp index cf3c18b00..75667985f 100644 --- a/Source/panels/spell_list.cpp +++ b/Source/panels/spell_list.cpp @@ -1,8 +1,9 @@ -#include "panels/spell_list.hpp" - -#include - -#include +#include "panels/spell_list.hpp" + +#include +#include + +#include #include "control/control.hpp" #include "controls/control_mode.hpp" @@ -11,8 +12,9 @@ #include "engine/palette.h" #include "engine/render/primitive_render.hpp" #include "engine/render/text_render.hpp" -#include "inv_iterators.hpp" -#include "options.h" +#include "inv_iterators.hpp" +#include "missiles.h" +#include "options.h" #include "panels/spell_icons.hpp" #include "player.h" #include "spells.h" @@ -67,7 +69,7 @@ bool GetSpellListSelection(SpellID &pSpell, SpellType &pSplType) return false; } -std::optional GetHotkeyName(SpellID spellId, SpellType spellType, bool useShortName = false) +std::optional GetHotkeyName(SpellID spellId, SpellType spellType, bool useShortName = false) { const Player &myPlayer = *MyPlayer; for (size_t t = 0; t < NumHotkeys; t++) { @@ -78,13 +80,99 @@ std::optional GetHotkeyName(SpellID spellId, SpellType spellTy return GetOptions().Padmapper.InputNameForAction(quickSpellActionKey, useShortName); return GetOptions().Keymapper.KeyNameForAction(quickSpellActionKey); } - return {}; -} - -} // namespace - -void DrawSpell(const Surface &out) -{ + return {}; +} + +std::string GetSpellPowerTextForSpeech(SpellID spell, int spellLevel) +{ + if (spellLevel == 0) { + return {}; + } + if (spell == SpellID::BoneSpirit) { + return std::string(_(/* TRANSLATORS: UI constraints, keep short please.*/ "Dmg: 1/3 target hp")); + } + const auto [min, max] = GetDamageAmt(spell, spellLevel); + if (min == -1) { + return {}; + } + if (spell == SpellID::Healing || spell == SpellID::HealOther) { + return fmt::format(fmt::runtime(_(/* TRANSLATORS: UI constraints, keep short please.*/ "Heals: {:d} - {:d}")), min, max); + } + return fmt::format(fmt::runtime(_(/* TRANSLATORS: UI constraints, keep short please.*/ "Damage: {:d} - {:d}")), min, max); +} + +void AppendSpellSpeechSegment(std::string &speech, std::string_view segment) +{ + if (segment.empty()) { + return; + } + if (!speech.empty()) { + StrAppend(speech, ", "); + } + StrAppend(speech, segment); +} + +} // namespace + +std::string BuildSpellDetailsForSpeech(const Player &player, SpellID spellId, SpellType spellType) +{ + if (!IsValidSpell(spellId)) { + return std::string(_("No spell selected.")); + } + + if (spellId == GetPlayerStartingLoadoutForClass(player._pClass).skill) { + spellType = SpellType::Skill; + } + + const std::string_view spellName = pgettext("spell", GetSpellData(spellId).sNameText); + std::string speech; + + switch (spellType) { + case SpellType::Skill: + AppendSpellSpeechSegment(speech, fmt::format(fmt::runtime(_("{:s} Skill")), spellName)); + break; + case SpellType::Spell: { + AppendSpellSpeechSegment(speech, fmt::format(fmt::runtime(_("{:s} Spell")), spellName)); + if (spellId == SpellID::HolyBolt) { + AppendSpellSpeechSegment(speech, _("Damages undead only")); + } + const int spellLevel = player.GetSpellLevel(spellId); + AppendSpellSpeechSegment(speech, + spellLevel == 0 ? std::string(_("Spell Level 0 - Unusable")) : fmt::format(fmt::runtime(_("Spell Level {:d}")), spellLevel)); + if (spellLevel > 0) { + const std::string power = GetSpellPowerTextForSpeech(spellId, spellLevel); + if (!power.empty()) { + AppendSpellSpeechSegment(speech, power); + } + const int mana = GetManaAmount(player, spellId) >> 6; + AppendSpellSpeechSegment(speech, fmt::format(fmt::runtime(pgettext("spellbook", "Mana: {:d}")), mana)); + } + } break; + case SpellType::Scroll: { + AppendSpellSpeechSegment(speech, fmt::format(fmt::runtime(_("Scroll of {:s}")), spellName)); + const int scrollCount = c_count_if(InventoryAndBeltPlayerItemsRange { player }, [spellId](const Item &item) { + return item.isScrollOf(spellId); + }); + AppendSpellSpeechSegment(speech, fmt::format(fmt::runtime(ngettext("{:d} Scroll", "{:d} Scrolls", scrollCount)), scrollCount)); + } break; + case SpellType::Charges: { + AppendSpellSpeechSegment(speech, fmt::format(fmt::runtime(_("Staff of {:s}")), spellName)); + const int charges = player.InvBody[INVLOC_HAND_LEFT]._iCharges; + AppendSpellSpeechSegment(speech, fmt::format(fmt::runtime(ngettext("{:d} Charge", "{:d} Charges", charges)), charges)); + } break; + case SpellType::Invalid: + AppendSpellSpeechSegment(speech, spellName); + break; + } + + if (speech.empty()) { + speech = spellName; + } + return speech; +} + +void DrawSpell(const Surface &out) +{ const Player &myPlayer = *MyPlayer; SpellID spl = myPlayer._pRSpell; SpellType st = myPlayer._pRSplType; @@ -329,7 +417,7 @@ void ToggleSpell(size_t slot) myPlayer._pRSpell = myPlayer._pSplHotKey[slot]; myPlayer._pRSplType = myPlayer._pSplTHotKey[slot]; RedrawEverything(); - SpeakText(pgettext("spell", GetSpellData(myPlayer._pRSpell).sNameText), /*force=*/true); + SpeakText(BuildSpellDetailsForSpeech(myPlayer, myPlayer._pRSpell, myPlayer._pRSplType), /*force=*/true); } } diff --git a/Source/panels/spell_list.hpp b/Source/panels/spell_list.hpp index 0b706f96a..0bec141aa 100644 --- a/Source/panels/spell_list.hpp +++ b/Source/panels/spell_list.hpp @@ -1,7 +1,8 @@ -#pragma once - -#include -#include +#pragma once + +#include +#include +#include #include "engine/point.hpp" #include "engine/surface.hpp" @@ -9,12 +10,14 @@ namespace devilution { -struct SpellListItem { - Point location; - SpellType type; - SpellID id; - bool isSelected; -}; +struct SpellListItem { + Point location; + SpellType type; + SpellID id; + bool isSelected; +}; + +struct Player; /** * @brief draws the current right mouse button spell. @@ -25,8 +28,9 @@ void DrawSpellList(const Surface &out); std::vector GetSpellListItems(); void SetSpell(); void SetSpeedSpell(size_t slot); -bool IsValidSpeedSpell(size_t slot); -void ToggleSpell(size_t slot); +bool IsValidSpeedSpell(size_t slot); +void ToggleSpell(size_t slot); +std::string BuildSpellDetailsForSpeech(const Player &player, SpellID spellId, SpellType spellType); /** * Draws the "Speed Book": the rows of known spells for quick-setting a spell that diff --git a/VERSION b/VERSION index 6261a05bb..88c5fb891 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.3.1 \ No newline at end of file +1.4.0