diff --git a/Source/DiabloUI/diabloui.cpp b/Source/DiabloUI/diabloui.cpp index 4cf221436..b308bcf8b 100644 --- a/Source/DiabloUI/diabloui.cpp +++ b/Source/DiabloUI/diabloui.cpp @@ -109,14 +109,16 @@ void (*gfnFullscreen)(); bool (*gfnListYesNo)(); std::vector gUiItems; UiList *gUiList = nullptr; -bool UiItemsWraps; - -std::optional UiTextInputState; -bool allowEmptyTextInput = false; - -constexpr Uint32 ListDoubleClickTimeMs = 500; -std::size_t lastListClickIndex = static_cast(-1); -Uint32 lastListClickTicks = 0; +bool UiItemsWraps; + +std::optional UiTextInputState; +bool allowEmptyTextInput = false; + +std::optional UiSpokenTextOverride; + +constexpr Uint32 ListDoubleClickTimeMs = 500; +std::size_t lastListClickIndex = static_cast(-1); +Uint32 lastListClickTicks = 0; struct ScrollBarState { bool upArrowPressed; @@ -155,14 +157,20 @@ std::string FormatSpokenText(const StringOrView &format, const std::vector SelectedItemMax) - return; - - const UiListItem *pItem = gUiList->GetItem(index); - if (pItem == nullptr) - return; +void SpeakListItem(std::size_t index, bool force = false) +{ + if (gUiList == nullptr || index > SelectedItemMax) + return; + + if (UiSpokenTextOverride) { + SpeakText(*UiSpokenTextOverride, force); + UiSpokenTextOverride = std::nullopt; + return; + } + + const UiListItem *pItem = gUiList->GetItem(index); + if (pItem == nullptr) + return; std::string text = FormatSpokenText(pItem->m_text, pItem->args); @@ -180,10 +188,10 @@ void SpeakListItem(std::size_t index, bool force = false) if (!text.empty()) SpeakText(text, force); -} - -void AdjustListOffset(std::size_t itemIndex) -{ +} + +void AdjustListOffset(std::size_t itemIndex) +{ if (itemIndex >= listOffset + ListViewportSize) listOffset = itemIndex - (ListViewportSize - 1); if (itemIndex < listOffset) @@ -232,13 +240,18 @@ void UiUpdateFadePalette() SystemPaletteUpdated(); if (IsHardwareCursor()) ReinitializeHardwareCursor(); } - -} // namespace - -bool IsTextInputActive() -{ - return UiTextInputState.has_value(); -} + +} // namespace + +void UiSetSpokenTextOverride(std::string text) +{ + UiSpokenTextOverride = std::move(text); +} + +bool IsTextInputActive() +{ + return UiTextInputState.has_value(); +} void UiInitList(void (*fnFocus)(size_t value), void (*fnSelect)(size_t value), void (*fnEsc)(), const std::vector> &items, bool itemsWraps, void (*fnFullscreen)(), bool (*fnYesNo)(), size_t selectedItem /*= 0*/) { @@ -365,19 +378,19 @@ void UiFocus(std::size_t itemIndex, bool checkUp, bool ignoreItemsWraps = false) } pItem = gUiList->GetItem(itemIndex); } - SpeakListItem(itemIndex); - - if (HasAnyOf(pItem->uiFlags, UiFlags::NeedsNextElement)) - AdjustListOffset(itemIndex + 1); - AdjustListOffset(itemIndex); - - SelectedItem = itemIndex; - - UiPlayMoveSound(); - - if (gfnListFocus != nullptr) - gfnListFocus(itemIndex); -} + if (HasAnyOf(pItem->uiFlags, UiFlags::NeedsNextElement)) + AdjustListOffset(itemIndex + 1); + AdjustListOffset(itemIndex); + + SelectedItem = itemIndex; + + UiPlayMoveSound(); + + if (gfnListFocus != nullptr) + gfnListFocus(itemIndex); + + SpeakListItem(itemIndex); +} void UiFocusUp() { diff --git a/Source/DiabloUI/diabloui.h b/Source/DiabloUI/diabloui.h index 61c5cd7a8..addc45dc8 100644 --- a/Source/DiabloUI/diabloui.h +++ b/Source/DiabloUI/diabloui.h @@ -1,13 +1,15 @@ -#pragma once - -#include -#include -#include -#include - -#ifdef USE_SDL3 -#include -#include +#pragma once + +#include +#include +#include +#include +#include +#include + +#ifdef USE_SDL3 +#include +#include #else #include #endif @@ -109,13 +111,16 @@ bool UiLoadBlackBackground(); void LoadBackgroundArt(const char *pszFile, int frames = 1); void UiAddBackground(std::vector> *vecDialog); void UiAddLogo(std::vector> *vecDialog, int y = GetUIRectangle().position.y); -void UiFocusNavigationSelect(); -void UiFocusNavigationEsc(); -void UiFocusNavigationYesNo(); - -void UiInitList(void (*fnFocus)(size_t value), void (*fnSelect)(size_t value), void (*fnEsc)(), const std::vector> &items, bool wraps = false, void (*fnFullscreen)() = nullptr, bool (*fnYesNo)() = nullptr, size_t selectedItem = 0); -void UiRenderListItems(); -void UiInitList_clear(); +void UiFocusNavigationSelect(); +void UiFocusNavigationEsc(); +void UiFocusNavigationYesNo(); + +/** Overrides what the screen reader will speak for the next focused list item. */ +void UiSetSpokenTextOverride(std::string text); + +void UiInitList(void (*fnFocus)(size_t value), void (*fnSelect)(size_t value), void (*fnEsc)(), const std::vector> &items, bool wraps = false, void (*fnFullscreen)() = nullptr, bool (*fnYesNo)() = nullptr, size_t selectedItem = 0); +void UiRenderListItems(); +void UiInitList_clear(); void UiClearScreen(); void UiPollAndRender(std::optional> eventHandler = std::nullopt); diff --git a/Source/DiabloUI/hero/selhero.cpp b/Source/DiabloUI/hero/selhero.cpp index 883b4c1f4..8292d05f5 100644 --- a/Source/DiabloUI/hero/selhero.cpp +++ b/Source/DiabloUI/hero/selhero.cpp @@ -65,23 +65,43 @@ std::vector> vecSelHeroDialog; std::vector> vecSelHeroDlgItems; std::vector> vecSelDlgItems; -UiImageClx *SELHERO_DIALOG_HERO_IMG; - -void SelheroListFocus(size_t value); -void SelheroListSelect(size_t value); -void SelheroListEsc(); -void SelheroLoadFocus(size_t value); -void SelheroLoadSelect(size_t value); -void SelheroNameSelect(size_t value); -void SelheroNameEsc(); -void SelheroClassSelectorFocus(size_t value); -void SelheroClassSelectorSelect(size_t value); -void SelheroClassSelectorEsc(); -const char *SelheroGenerateName(HeroClass heroClass); - -void SelheroUiFocusNavigationYesNo() -{ - if (selhero_isSavegame) +UiImageClx *SELHERO_DIALOG_HERO_IMG; + +void SelheroListFocus(size_t value); +void SelheroListSelect(size_t value); +void SelheroListEsc(); +void SelheroLoadFocus(size_t value); +void SelheroLoadSelect(size_t value); +void SelheroNameSelect(size_t value); +void SelheroNameEsc(); +void SelheroClassSelectorFocus(size_t value); +void SelheroClassSelectorSelect(size_t value); +void SelheroClassSelectorEsc(); +const char *SelheroGenerateName(HeroClass heroClass); + +std::string_view HeroClassDescriptionForSpeech(HeroClass heroClass) +{ + switch (heroClass) { + case HeroClass::Warrior: + return _("A powerful fighter who excels in melee combat."); + case HeroClass::Rogue: + return _("A nimble archer who excels at ranged combat."); + case HeroClass::Sorcerer: + return _("A master of arcane magic who casts powerful spells."); + case HeroClass::Monk: + return _("A holy warrior skilled in martial arts and staves."); + case HeroClass::Bard: + return _("A versatile fighter who blends melee and archery."); + case HeroClass::Barbarian: + return _("A fierce warrior who relies on brute strength."); + default: + return {}; + } +} + +void SelheroUiFocusNavigationYesNo() +{ + if (selhero_isSavegame) UiFocusNavigationYesNo(); } @@ -248,22 +268,31 @@ void SelheroListEsc() selhero_result = SELHERO_PREVIOUS; } -void SelheroClassSelectorFocus(size_t value) -{ - const auto heroClass = static_cast(vecSelHeroDlgItems[value]->m_value); - - _uidefaultstats defaults; - gfnHeroStats(heroClass, &defaults); +void SelheroClassSelectorFocus(size_t value) +{ + const auto heroClass = static_cast(vecSelHeroDlgItems[value]->m_value); + + _uidefaultstats defaults; + gfnHeroStats(heroClass, &defaults); selhero_heroInfo.level = 1; selhero_heroInfo.heroclass = heroClass; selhero_heroInfo.strength = defaults.strength; selhero_heroInfo.magic = defaults.magic; - selhero_heroInfo.dexterity = defaults.dexterity; - selhero_heroInfo.vitality = defaults.vitality; - - SelheroSetStats(); -} + selhero_heroInfo.dexterity = defaults.dexterity; + selhero_heroInfo.vitality = defaults.vitality; + + SelheroSetStats(); + + const PlayerData &playerData = GetPlayerDataForClass(heroClass); + const std::string_view description = HeroClassDescriptionForSpeech(heroClass); + std::string spoken = std::string(_(playerData.className)); + if (!description.empty()) { + spoken.append("\n"); + spoken.append(description); + } + UiSetSpokenTextOverride(std::move(spoken)); +} bool ShouldPrefillHeroName() { diff --git a/Source/DiabloUI/multi/selgame.cpp b/Source/DiabloUI/multi/selgame.cpp index dc8af704d..eb556ad52 100644 --- a/Source/DiabloUI/multi/selgame.cpp +++ b/Source/DiabloUI/multi/selgame.cpp @@ -409,24 +409,38 @@ void selgame_GameSelection_Esc() selgame_endMenu = true; } -void selgame_Diff_Focus(size_t value) -{ - switch (vecSelGameDlgItems[value]->m_value) { - case DIFF_NORMAL: - CopyUtf8(selgame_Label, _("Normal"), sizeof(selgame_Label)); - CopyUtf8(selgame_Description, _("Normal Difficulty\nThis is where a starting character should begin the quest to defeat Diablo."), sizeof(selgame_Description)); - break; - case DIFF_NIGHTMARE: - CopyUtf8(selgame_Label, _("Nightmare"), sizeof(selgame_Label)); - CopyUtf8(selgame_Description, _("Nightmare Difficulty\nThe denizens of the Labyrinth have been bolstered and will prove to be a greater challenge. This is recommended for experienced characters only."), sizeof(selgame_Description)); - break; - case DIFF_HELL: - CopyUtf8(selgame_Label, _("Hell"), sizeof(selgame_Label)); - CopyUtf8(selgame_Description, _("Hell Difficulty\nThe most powerful of the underworld's creatures lurk at the gateway into Hell. Only the most experienced characters should venture in this realm."), sizeof(selgame_Description)); - break; - } - CopyUtf8(selgame_Description, WordWrapString(selgame_Description, DESCRIPTION_WIDTH), sizeof(selgame_Description)); -} +void selgame_Diff_Focus(size_t value) +{ + std::string_view tooltip; + switch (vecSelGameDlgItems[value]->m_value) { + case DIFF_NORMAL: + CopyUtf8(selgame_Label, _("Normal"), sizeof(selgame_Label)); + tooltip = _("Normal Difficulty\nThis is where a starting character should begin the quest to defeat Diablo."); + CopyUtf8(selgame_Description, tooltip, sizeof(selgame_Description)); + break; + case DIFF_NIGHTMARE: + CopyUtf8(selgame_Label, _("Nightmare"), sizeof(selgame_Label)); + tooltip = _("Nightmare Difficulty\nThe denizens of the Labyrinth have been bolstered and will prove to be a greater challenge. This is recommended for experienced characters only."); + CopyUtf8(selgame_Description, tooltip, sizeof(selgame_Description)); + break; + case DIFF_HELL: + CopyUtf8(selgame_Label, _("Hell"), sizeof(selgame_Label)); + tooltip = _("Hell Difficulty\nThe most powerful of the underworld's creatures lurk at the gateway into Hell. Only the most experienced characters should venture in this realm."); + CopyUtf8(selgame_Description, tooltip, sizeof(selgame_Description)); + break; + } + CopyUtf8(selgame_Description, WordWrapString(selgame_Description, DESCRIPTION_WIDTH), sizeof(selgame_Description)); + + std::string spoken = selgame_Label; + std::string_view spokenDescription = tooltip; + if (const size_t newlinePos = spokenDescription.find('\n'); newlinePos != std::string_view::npos) + spokenDescription = spokenDescription.substr(newlinePos + 1); + if (!spokenDescription.empty()) { + spoken.append("\n"); + spoken.append(spokenDescription); + } + UiSetSpokenTextOverride(std::move(spoken)); +} bool IsDifficultyAllowed(int value) { diff --git a/Translations/pl.po b/Translations/pl.po index 0ff7be6ff..1beb093c5 100644 --- a/Translations/pl.po +++ b/Translations/pl.po @@ -5409,6 +5409,30 @@ msgstr "Barda" msgid "Barbarian" msgstr "Barbarzyńca" +#: Source/DiabloUI/hero/selhero.cpp:86 +msgid "A powerful fighter who excels in melee combat." +msgstr "Potężny wojownik, który świetnie radzi sobie w walce wręcz." + +#: Source/DiabloUI/hero/selhero.cpp:88 +msgid "A nimble archer who excels at ranged combat." +msgstr "Zwinna łuczniczka, która świetnie radzi sobie w walce na dystans." + +#: Source/DiabloUI/hero/selhero.cpp:90 +msgid "A master of arcane magic who casts powerful spells." +msgstr "Mistrz magii tajemnej, który rzuca potężne zaklęcia." + +#: Source/DiabloUI/hero/selhero.cpp:92 +msgid "A holy warrior skilled in martial arts and staves." +msgstr "Święty wojownik biegły w sztukach walki i władaniu kosturami." + +#: Source/DiabloUI/hero/selhero.cpp:94 +msgid "A versatile fighter who blends melee and archery." +msgstr "Wszechstronna wojowniczka, która łączy walkę wręcz i łucznictwo." + +#: Source/DiabloUI/hero/selhero.cpp:96 +msgid "A fierce warrior who relies on brute strength." +msgstr "Zaciekły wojownik, który polega na brutalnej sile." + #: Source/translation_dummy.cpp:17 msgctxt "monster" msgid "Zombie"