[Scummvm-git-logs] scummvm master -> ebe27c7fa312699ef2790038bc360fbbd7d73d4c
sev-
noreply at scummvm.org
Sun Aug 10 20:38:22 UTC 2025
This automated email contains information about 1 new commit which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
ebe27c7fa3 MM: Add text-to-speech (TTS)
Commit: ebe27c7fa312699ef2790038bc360fbbd7d73d4c
https://github.com/scummvm/scummvm/commit/ebe27c7fa312699ef2790038bc360fbbd7d73d4c
Author: ellm135 (ellm13531 at gmail.com)
Date: 2025-08-10T22:38:19+02:00
Commit Message:
MM: Add text-to-speech (TTS)
Changed paths:
engines/mm/detection.h
engines/mm/detection_tables.h
engines/mm/metaengine.cpp
engines/mm/mm1/mm1.cpp
engines/mm/mm1/views/text_view.cpp
engines/mm/xeen/dialogs/credits_screen.cpp
engines/mm/xeen/dialogs/credits_screen.h
engines/mm/xeen/dialogs/dialogs.cpp
engines/mm/xeen/dialogs/dialogs.h
engines/mm/xeen/dialogs/dialogs_awards.cpp
engines/mm/xeen/dialogs/dialogs_char_info.cpp
engines/mm/xeen/dialogs/dialogs_char_info.h
engines/mm/xeen/dialogs/dialogs_control_panel.cpp
engines/mm/xeen/dialogs/dialogs_control_panel.h
engines/mm/xeen/dialogs/dialogs_copy_protection.cpp
engines/mm/xeen/dialogs/dialogs_copy_protection.h
engines/mm/xeen/dialogs/dialogs_create_char.cpp
engines/mm/xeen/dialogs/dialogs_create_char.h
engines/mm/xeen/dialogs/dialogs_difficulty.cpp
engines/mm/xeen/dialogs/dialogs_dismiss.cpp
engines/mm/xeen/dialogs/dialogs_info.cpp
engines/mm/xeen/dialogs/dialogs_info.h
engines/mm/xeen/dialogs/dialogs_input.cpp
engines/mm/xeen/dialogs/dialogs_items.cpp
engines/mm/xeen/dialogs/dialogs_items.h
engines/mm/xeen/dialogs/dialogs_map.cpp
engines/mm/xeen/dialogs/dialogs_message.cpp
engines/mm/xeen/dialogs/dialogs_party.cpp
engines/mm/xeen/dialogs/dialogs_party.h
engines/mm/xeen/dialogs/dialogs_query.cpp
engines/mm/xeen/dialogs/dialogs_quests.cpp
engines/mm/xeen/dialogs/dialogs_quick_fight.cpp
engines/mm/xeen/dialogs/dialogs_quick_fight.h
engines/mm/xeen/dialogs/dialogs_quick_ref.cpp
engines/mm/xeen/dialogs/dialogs_quick_ref.h
engines/mm/xeen/dialogs/dialogs_spells.cpp
engines/mm/xeen/dialogs/dialogs_spells.h
engines/mm/xeen/dialogs/dialogs_whowill.cpp
engines/mm/xeen/dialogs/dialogs_whowill.h
engines/mm/xeen/events.cpp
engines/mm/xeen/font.cpp
engines/mm/xeen/font.h
engines/mm/xeen/interface.cpp
engines/mm/xeen/interface.h
engines/mm/xeen/locations.cpp
engines/mm/xeen/locations.h
engines/mm/xeen/party.cpp
engines/mm/xeen/scripts.cpp
engines/mm/xeen/subtitles.cpp
engines/mm/xeen/swordsofxeen/swordsofxeen_menu.cpp
engines/mm/xeen/window.h
engines/mm/xeen/worldofxeen/clouds_cutscenes.cpp
engines/mm/xeen/worldofxeen/worldofxeen_menu.cpp
engines/mm/xeen/worldofxeen/worldofxeen_menu.h
engines/mm/xeen/xeen.cpp
engines/mm/xeen/xeen.h
diff --git a/engines/mm/detection.h b/engines/mm/detection.h
index 6b8d9605a8c..850fb39d874 100644
--- a/engines/mm/detection.h
+++ b/engines/mm/detection.h
@@ -53,6 +53,7 @@ struct MightAndMagicGameDescription {
#define GAMEOPTION_DURABLE_ARMOR GUIO_GAMEOPTIONS2
#define GAMEOPTION_SHOW_HP_SP_BARS GUIO_GAMEOPTIONS3
#define GAMEOPTION_COPY_PROTECTION GUIO_GAMEOPTIONS4
+#define GAMEOPTION_TTS GUIO_GAMEOPTIONS5
} // namespace MM
diff --git a/engines/mm/detection_tables.h b/engines/mm/detection_tables.h
index 0c200477213..e332145354c 100644
--- a/engines/mm/detection_tables.h
+++ b/engines/mm/detection_tables.h
@@ -24,7 +24,7 @@
namespace MM {
-#define GUIO_XEEN GUIO4(GAMEOPTION_SHOW_ITEM_COSTS, GAMEOPTION_DURABLE_ARMOR, GAMEOPTION_SHOW_HP_SP_BARS, GAMEOPTION_COPY_PROTECTION)
+#define GUIO_XEEN GUIO5(GAMEOPTION_SHOW_ITEM_COSTS, GAMEOPTION_DURABLE_ARMOR, GAMEOPTION_SHOW_HP_SP_BARS, GAMEOPTION_COPY_PROTECTION, GAMEOPTION_TTS)
static const MightAndMagicGameDescription GAME_DESCRIPTIONS[] = {
{
@@ -36,7 +36,7 @@ static const MightAndMagicGameDescription GAME_DESCRIPTIONS[] = {
Common::EN_ANY,
Common::kPlatformDOS,
ADGF_NO_FLAGS,
- GUIO0()
+ GUIO1(GAMEOPTION_TTS)
},
GType_MightAndMagic1,
0
@@ -51,7 +51,7 @@ static const MightAndMagicGameDescription GAME_DESCRIPTIONS[] = {
Common::EN_ANY,
Common::kPlatformDOS,
ADGF_NO_FLAGS,
- GUIO0()
+ GUIO1(GAMEOPTION_TTS)
},
GType_MightAndMagic1,
GF_ENHANCED
@@ -68,7 +68,7 @@ static const MightAndMagicGameDescription GAME_DESCRIPTIONS[] = {
Common::EN_ANY,
Common::kPlatformDOS,
ADGF_NO_FLAGS,
- GUIO0()
+ GUIO1(GAMEOPTION_TTS)
},
GType_MightAndMagic1,
GF_GFX_PACK
diff --git a/engines/mm/metaengine.cpp b/engines/mm/metaengine.cpp
index 1a4c1690fd8..6a96fcaeeac 100644
--- a/engines/mm/metaengine.cpp
+++ b/engines/mm/metaengine.cpp
@@ -84,6 +84,19 @@ static const ADExtraGuiOptionsMap optionsList[] = {
0
}
},
+#ifdef USE_TTS
+ {
+ GAMEOPTION_TTS,
+ {
+ _s("Enable Text to Speech"),
+ _s("Use TTS to read text in the game (if TTS is available)"),
+ "tts_enabled",
+ false,
+ 0,
+ 0
+ }
+ },
+#endif
AD_EXTRA_GUI_OPTIONS_TERMINATOR
};
diff --git a/engines/mm/mm1/mm1.cpp b/engines/mm/mm1/mm1.cpp
index b9894a2b7a4..f942a7e26fe 100644
--- a/engines/mm/mm1/mm1.cpp
+++ b/engines/mm/mm1/mm1.cpp
@@ -24,6 +24,7 @@
#include "common/debug-channels.h"
#include "common/file.h"
#include "common/system.h"
+#include "common/text-to-speech.h"
#include "common/translation.h"
#include "engines/util.h"
#include "graphics/paletteman.h"
@@ -73,6 +74,14 @@ Common::Error MM1Engine::run() {
_sound = new Sound(_mixer);
syncSoundSettings();
+#ifdef USE_TTS
+ Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();
+ if (ttsMan != nullptr) {
+ ttsMan->enable(ConfMan.getBool("tts_enabled"));
+ ttsMan->setLanguage(ConfMan.get("language"));
+ }
+#endif
+
// Setup console
setDebugger(new Console());
if (gDebugLevel > 0)
diff --git a/engines/mm/mm1/views/text_view.cpp b/engines/mm/mm1/views/text_view.cpp
index 6884348916c..cfdaea97992 100644
--- a/engines/mm/mm1/views/text_view.cpp
+++ b/engines/mm/mm1/views/text_view.cpp
@@ -19,6 +19,8 @@
*
*/
+#include "common/text-to-speech.h"
+
#include "mm/mm1/globals.h"
#include "mm/mm1/gfx/gfx.h"
#include "mm/mm1/views/text_view.h"
@@ -59,6 +61,13 @@ void TextView::writeChar(int x, int y, unsigned char c) {
}
void TextView::writeString(const Common::String &str) {
+#ifdef USE_TTS
+ Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();
+ if (ttsMan && ConfMan.getBool("tts_enabled")) {
+ ttsMan->say(str, Common::CodePage::kDos850);
+ }
+#endif
+
for (const unsigned char *s = (const unsigned char *)str.c_str(); *s; ++s) {
writeChar(*s);
diff --git a/engines/mm/xeen/dialogs/credits_screen.cpp b/engines/mm/xeen/dialogs/credits_screen.cpp
index 6acb382da5b..0c0b85d7637 100644
--- a/engines/mm/xeen/dialogs/credits_screen.cpp
+++ b/engines/mm/xeen/dialogs/credits_screen.cpp
@@ -55,9 +55,14 @@ void CreditsScreen::execute(const char *content) {
windows[GAME_WINDOW].close();
screen.loadBackground("marb.raw");
- windows[0].writeString(content);
+ Common::String ttsMessage;
+ windows[0].writeString(content, false, &ttsMessage);
doScroll(false, false);
+#ifdef USE_TTS
+ speakText(ttsMessage, (_vm->getGameID() != GType_Swords || content == Res.SWORDS_CREDITS1));
+#endif
+
events.setCursor(0);
windows[0].update();
clearButtons();
@@ -66,8 +71,60 @@ void CreditsScreen::execute(const char *content) {
while (!_vm->shouldExit() && !events.isKeyMousePressed())
events.pollEventsAndWait();
+#ifdef USE_TTS
+ _vm->stopTextToSpeech();
+#endif
doScroll(true, false);
}
+#ifdef USE_TTS
+
+void CreditsScreen::speakText(const Common::String &text, bool firstCreditsScreen) const {
+ if (_vm->getGameID() == GType_Swords && firstCreditsScreen) {
+ uint index = 0;
+ // Developed/published by
+ _vm->sayText(getNextTextSection(text, index, 2));
+
+ // Next four headers are separate from their corresponding credits. First get the headers, then voice the person
+ // for the first header, and then voice the second header and second person
+ for (uint8 i = 0; i < 2; ++i) {
+ _vm->sayText(getNextTextSection(text, index));
+ Common::String nextHeader = getNextTextSection(text, index);
+ _vm->sayText(getNextTextSection(text, index));
+ _vm->sayText(nextHeader);
+ _vm->sayText(getNextTextSection(text, index));
+ }
+
+ // Same as first four headers, but with two people at a time instead of one
+ for (uint8 i = 0; i < 2; ++i) {
+ // First two headers
+ _vm->sayText(getNextTextSection(text, index));
+ Common::String nextHeader = getNextTextSection(text, index);
+
+ // First people listed
+ _vm->sayText(getNextTextSection(text, index));
+ Common::String nextHeaderPerson = getNextTextSection(text, index);
+
+ // Second person listed under first header
+ _vm->sayText(getNextTextSection(text, index));
+
+ // Next header
+ _vm->sayText(nextHeader);
+ _vm->sayText(nextHeaderPerson);
+
+ // Second person for second header
+ if (i == 0) {
+ _vm->sayText(getNextTextSection(text, index));
+ } else { // Last header has more than two people
+ _vm->sayText(text.substr(index));
+ }
+ }
+ } else {
+ _vm->sayText(text);
+ }
+}
+
+#endif
+
} // End of namespace Xeen
} // End of namespace MM
diff --git a/engines/mm/xeen/dialogs/credits_screen.h b/engines/mm/xeen/dialogs/credits_screen.h
index d323a41720b..f3155838ab6 100644
--- a/engines/mm/xeen/dialogs/credits_screen.h
+++ b/engines/mm/xeen/dialogs/credits_screen.h
@@ -32,6 +32,10 @@ private:
CreditsScreen(XeenEngine *vm) : ButtonContainer(vm) {}
void execute(const char *content);
+
+#ifdef USE_TTS
+ void speakText(const Common::String &text, bool firstCreditsScreen) const;
+#endif
public:
static void show(XeenEngine *vm);
};
diff --git a/engines/mm/xeen/dialogs/dialogs.cpp b/engines/mm/xeen/dialogs/dialogs.cpp
index b246ba8d256..f7e21bf0038 100644
--- a/engines/mm/xeen/dialogs/dialogs.cpp
+++ b/engines/mm/xeen/dialogs/dialogs.cpp
@@ -46,13 +46,13 @@ void ButtonContainer::restoreButtons() {
}
void ButtonContainer::addButton(const Common::Rect &bounds, int val,
- SpriteResource *sprites) {
- _buttons.push_back(UIButton(bounds, val, _buttons.size() * 2, sprites, sprites != nullptr));
+ SpriteResource *sprites, uint8 ttsIndex) {
+ _buttons.push_back(UIButton(bounds, val, _buttons.size() * 2, sprites, sprites != nullptr, ttsIndex, ttsIndex < UINT8_MAX));
}
void ButtonContainer::addButton(const Common::Rect &bounds, int val,
- int frameNum, SpriteResource *sprites) {
- _buttons.push_back(UIButton(bounds, val, frameNum, sprites, sprites != nullptr));
+ int frameNum, SpriteResource *sprites, uint8 ttsIndex) {
+ _buttons.push_back(UIButton(bounds, val, frameNum, sprites, sprites != nullptr, ttsIndex, ttsIndex < UINT8_MAX));
}
void ButtonContainer::addPartyButtons(XeenEngine *vm) {
@@ -69,6 +69,11 @@ bool ButtonContainer::checkEvents(XeenEngine *vm) {
PendingEvent event;
_buttonValue = 0;
+#ifdef USE_TTS
+ bool buttonKeyboardPressed = false;
+ checkHoverOverButton();
+#endif
+
if (events.getEvent(event)) {
if (event._leftButton) {
Common::Point pt = events._mousePos;
@@ -111,8 +116,12 @@ bool ButtonContainer::checkEvents(XeenEngine *vm) {
&& keycode != Common::KEYCODE_LALT && keycode != Common::KEYCODE_RALT)
_buttonValue = keycode;
- if (_buttonValue)
+ if (_buttonValue) {
_buttonValue |= (event._keyState.flags & ~Common::KBD_STICKY) << 16;
+#ifdef USE_TTS
+ buttonKeyboardPressed = true;
+#endif
+ }
}
}
@@ -121,22 +130,32 @@ bool ButtonContainer::checkEvents(XeenEngine *vm) {
Window &win = windows[39];
for (uint btnIndex = 0; btnIndex < _buttons.size(); ++btnIndex) {
UIButton &btn = _buttons[btnIndex];
- if (btn._draw && btn._value == _buttonValue) {
- // Found the correct button
- // Draw button depressed
- btn._sprites->draw(0, btn._selectedFrame, Common::Point(btn._bounds.left, btn._bounds.top));
- win.setBounds(btn._bounds);
- win.update();
-
- // Slight delay
- events.updateGameCounter();
- events.wait(2);
-
- // Redraw button in it's original non-depressed form
- btn._sprites->draw(0, btn._frameNum, Common::Point(btn._bounds.left, btn._bounds.top));
- win.setBounds(btn._bounds);
- win.update();
- break;
+ if (btn._value == _buttonValue) {
+#ifdef USE_TTS
+ // Only voice the button's text if it was activated by keyboard press, to avoid voicing it
+ // every time the player clicks it
+ if (buttonKeyboardPressed && btn._canVoice && btn._ttsIndex < _buttonTexts.size()) {
+ _vm->sayText(_buttonTexts[btn._ttsIndex], Common::TextToSpeechManager::INTERRUPT);
+ }
+#endif
+
+ if (btn._draw) {
+ // Found the correct button
+ // Draw button depressed
+ btn._sprites->draw(0, btn._selectedFrame, Common::Point(btn._bounds.left, btn._bounds.top));
+ win.setBounds(btn._bounds);
+ win.update();
+
+ // Slight delay
+ events.updateGameCounter();
+ events.wait(2);
+
+ // Redraw button in it's original non-depressed form
+ btn._sprites->draw(0, btn._frameNum, Common::Point(btn._bounds.left, btn._bounds.top));
+ win.setBounds(btn._bounds);
+ win.update();
+ break;
+ }
}
}
@@ -146,6 +165,37 @@ bool ButtonContainer::checkEvents(XeenEngine *vm) {
return false;
}
+#ifdef USE_TTS
+
+void ButtonContainer::checkHoverOverButton() {
+ if (g_vm->_mouseMoved) {
+ bool hoveringOverButton = false;
+
+ Common::Point pt = g_vm->_events->_mousePos;
+
+ for (uint i = 0; i < _buttons.size(); ++i) {
+ if (_buttons[i]._canVoice && _buttons[i]._ttsIndex < _buttonTexts.size() && _buttons[i]._bounds.contains(pt) && _buttons[i]._value) {
+ hoveringOverButton = true;
+
+ if (_previousButton != (int)i) {
+ _vm->sayText(_buttonTexts[_buttons[i]._ttsIndex], Common::TextToSpeechManager::INTERRUPT);
+ _previousButton = (int)i;
+ }
+
+ break;
+ }
+ }
+
+ if (!hoveringOverButton) {
+ _previousButton = -1;
+ }
+
+ g_vm->_mouseMoved = false;
+ }
+}
+
+#endif
+
void ButtonContainer::drawButtons(XSurface *surface) {
for (uint btnIndex = 0; btnIndex < _buttons.size(); ++btnIndex) {
UIButton &btn = _buttons[btnIndex];
@@ -189,6 +239,99 @@ void ButtonContainer::setWaitBounds() {
_waitBounds = Common::Rect(8, 8, 224, 140);
}
+#ifdef USE_TTS
+
+Common::String ButtonContainer::getNextTextSection(const Common::String &text, uint &index, uint count, const char *separator) const {
+ Common::String result;
+ for (uint i = 0; i < count; ++i) {
+ result += getNextTextSection(text, index) + separator;
+
+ if (index == Common::String::npos) {
+ break;
+ }
+ }
+
+ return result;
+}
+
+Common::String ButtonContainer::getNextTextSection(const Common::String &text, uint &index, uint count) const {
+ return getNextTextSection(text, index, count, "\n");
+}
+
+Common::String ButtonContainer::getNextTextSection(const Common::String &text, uint &index) const {
+ Common::String result;
+
+ index = text.findFirstNotOf('\n', index);
+ if (index == Common::String::npos) {
+ return result;
+ }
+
+ uint endIndex = text.find('\n', index + 1);
+
+ if (endIndex == Common::String::npos) {
+ result = text.substr(index);
+ index = endIndex;
+ return result;
+ }
+
+ result = text.substr(index, endIndex - index);
+ index = endIndex + 1;
+ return result;
+}
+
+Common::String ButtonContainer::addNextTextToButtons(const Common::String &text, uint &index) {
+ Common::String buttonText = getNextTextSection(text, index) + '\n';
+ _buttonTexts.push_back(buttonText);
+ return buttonText;
+}
+
+Common::String ButtonContainer::addNextTextToButtons(const Common::String &text, uint &index, uint count) {
+ Common::String result;
+ for (uint i = 0; i < count; ++i) {
+ result += addNextTextToButtons(text, index);
+
+ if (index == Common::String::npos) {
+ break;
+ }
+ }
+ return result;
+}
+
+void ButtonContainer::setButtonTexts(const Common::String &text) {
+ _buttonTexts.clear();
+
+ uint index = 0;
+ for (uint i = 0; i < _buttons.size(); ++i) {
+ if (_buttons[i]._value) {
+ _buttonTexts.push_back(getNextTextSection(text, index));
+ } else {
+ _buttonTexts.push_back("");
+ }
+
+ if (index == Common::String::npos) {
+ break;
+ }
+ }
+}
+
+void ButtonContainer::disableButtonVoicing(uint startIndex, uint endIndex) {
+ for (uint i = startIndex; i < endIndex; ++i) {
+ if (i < _buttons.size()) {
+ _buttons[i]._canVoice = false;
+ }
+ }
+}
+
+void ButtonContainer::enableButtonVoicing(uint startIndex, uint endIndex) {
+ for (uint i = startIndex; i < endIndex; ++i) {
+ if (i < _buttons.size()) {
+ _buttons[i]._canVoice = true;
+ }
+ }
+}
+
+#endif
+
/*------------------------------------------------------------------------*/
void SettingsBaseDialog::showContents(SpriteResource &title1, bool waitFlag) {
diff --git a/engines/mm/xeen/dialogs/dialogs.h b/engines/mm/xeen/dialogs/dialogs.h
index d9ab5aa2fe2..141581d4bfe 100644
--- a/engines/mm/xeen/dialogs/dialogs.h
+++ b/engines/mm/xeen/dialogs/dialogs.h
@@ -41,18 +41,32 @@ public:
int _value;
uint _frameNum, _selectedFrame;
bool _draw;
+#ifdef USE_TTS
+ uint8 _ttsIndex;
+ bool _canVoice;
+#endif
/**
* Constructor
*/
- UIButton(const Common::Rect &bounds, int value, uint frameNum, SpriteResource *sprites, bool draw) :
+ UIButton(const Common::Rect &bounds, int value, uint frameNum, SpriteResource *sprites, bool draw, uint8 ttsIndex, bool ttsCanVoice) :
_bounds(bounds), _value(value), _frameNum(frameNum), _selectedFrame(frameNum | 1),
- _sprites(sprites), _draw(draw) {}
+ _sprites(sprites), _draw(draw) {
+#ifdef USE_TTS
+ _ttsIndex = ttsIndex;
+ _canVoice = ttsCanVoice;
+#endif
+ }
/**
* Constructor
*/
- UIButton() : _value(0), _frameNum(0), _selectedFrame(0), _sprites(nullptr), _draw(false) {}
+ UIButton() : _value(0), _frameNum(0), _selectedFrame(0), _sprites(nullptr), _draw(false) {
+#ifdef USE_TTS
+ _ttsIndex = 0;
+ _canVoice = false;
+#endif
+ }
/**
* Set the frame
@@ -79,6 +93,10 @@ protected:
Common::StringArray _textStrings;
Common::Rect _waitBounds;
int _buttonValue;
+#ifdef USE_TTS
+ Common::StringArray _buttonTexts;
+ int _previousButton;
+#endif
bool checkEvents(XeenEngine *vm);
@@ -108,8 +126,78 @@ protected:
* the equivalent of a space bar press, to the main interface area
*/
void setWaitBounds();
+
+#ifdef USE_TTS
+ /**
+ * Reads one or more sections of text, then combines them
+ * @param text Text to take the sections from. Each section should be separated by one or more newlines
+ * @param index Starting index, which is moved to the start of the following section, or npos if no sections remain
+ * @param count How many sections to read
+ * @param separator What to separate sections with
+ * @returns The text sections combined, each separated by the separator
+ */
+ Common::String getNextTextSection(const Common::String &text, uint &index, uint count, const char *separator) const;
+
+ /**
+ * Reads one or more sections of text, then combines them. Newlines are kept intact
+ * @param text Text to take the sections from. Each section should be separated by one or more newlines
+ * @param index Starting index, which is moved to the start of the following section, or npos if no sections remain
+ * @param count How many sections to read
+ * @returns The text sections combined
+ */
+ Common::String getNextTextSection(const Common::String &text, uint &index, uint count) const;
+
+ /**
+ * Reads one section of text
+ * @param text Text to take the section from. Each section should be separated by one or more newlines
+ * @param index Starting index, which is moved to the start of the following section, or npos if no sections remain
+ * @returns The text section
+ */
+ Common::String getNextTextSection(const Common::String &text, uint &index) const;
+
+ /**
+ * Reads one section of text and adds it to the button texts
+ * @param text Text to take the section from. Each section should be separated by one or more newlines
+ * @param index Starting index, which is moved to the start of the following section, or npos if no sections remain
+ * @returns The text section
+ */
+ Common::String addNextTextToButtons(const Common::String &text, uint &index);
+
+ /**
+ * Reads one or more sections of text and adds them to the button texts
+ * @param text Text to take the sections from. Each section should be separated by one or more newlines
+ * @param index Starting index, which is moved to the start of the following section, or npos if no sections remain
+ * @param count How many sections to read
+ * @returns The text sections combined
+ */
+ Common::String addNextTextToButtons(const Common::String &text, uint &index, uint count);
+
+ /**
+ * Sets the text of each button for use by TTS
+ * @param text Text for buttons. Each button's text should be separated by newlines
+ */
+ void setButtonTexts(const Common::String &text);
+
+ /**
+ * Disables the voicing of buttons from the start index to the end index
+ * @param startIndex Starting index
+ * @param endIndex Ending index
+ */
+ void disableButtonVoicing(uint startIndex, uint endIndex);
+
+ /**
+ * Enables the voicing of buttons from the start index to the end index
+ * @param startIndex Starting index
+ * @param endIndex Ending index
+ */
+ void enableButtonVoicing(uint startIndex, uint endIndex);
+#endif
public:
- ButtonContainer(XeenEngine *vm) : Cutscenes(vm), _buttonValue(0) {}
+ ButtonContainer(XeenEngine *vm) : Cutscenes(vm), _buttonValue(0) {
+#ifdef USE_TTS
+ _previousButton = -1;
+#endif
+ }
/**
* Saves the current list of buttons
@@ -121,9 +209,9 @@ public:
void restoreButtons();
void addButton(const Common::Rect &bounds, int val,
- SpriteResource *sprites = nullptr);
+ SpriteResource *sprites = nullptr, uint8 ttsIndex = UINT8_MAX);
void addButton(const Common::Rect &bounds, int val,
- int frameNum, SpriteResource *sprites = nullptr);
+ int frameNum, SpriteResource *sprites = nullptr, uint8 ttsIndex = UINT8_MAX);
void addPartyButtons(XeenEngine *vm);
@@ -136,6 +224,13 @@ public:
* Clears any currently set button value
*/
void clearEvents() { _buttonValue = 0; }
+
+#ifdef USE_TTS
+ /**
+ * Checks if a button is being hovered over and voices its text with TTS, if there is text
+ */
+ void checkHoverOverButton();
+#endif
};
class SettingsBaseDialog : public ButtonContainer {
diff --git a/engines/mm/xeen/dialogs/dialogs_awards.cpp b/engines/mm/xeen/dialogs/dialogs_awards.cpp
index 375fa005c12..aac65da7b16 100644
--- a/engines/mm/xeen/dialogs/dialogs_awards.cpp
+++ b/engines/mm/xeen/dialogs/dialogs_awards.cpp
@@ -27,6 +27,12 @@
namespace MM {
namespace Xeen {
+enum AwardsButtonTTSTextIndex {
+ kAwardsUp = 0,
+ kAwardsDown = 1,
+ kAwardsExit = 2
+};
+
void Awards::show(XeenEngine *vm, const Character *ch) {
Awards *dlg = new Awards(vm);
dlg->execute(ch);
@@ -51,8 +57,12 @@ void Awards::execute(const Character *ch) {
windows[30].open();
}
- windows[29].writeString(Res.AWARDS_TEXT);
+ Common::String buttonText;
+ windows[29].writeString(Res.AWARDS_TEXT, false, &buttonText);
drawButtons(&windows[0]);
+#ifdef USE_TTS
+ setButtonTexts(buttonText);
+#endif
while (!_vm->shouldExit()) {
// Build up a list of awards the character has
@@ -94,6 +104,9 @@ void Awards::execute(const Character *ch) {
awards[topIndex + 8].c_str()
);
windows[30].writeString(msg);
+#ifdef USE_TTS
+ _vm->sayText(buttonText);
+#endif
windows[24].update();
// Wait for keypress
@@ -123,9 +136,9 @@ void Awards::execute(const Character *ch) {
void Awards::addButtons() {
_iconSprites.load("award.icn");
- addButton(Common::Rect(216, 109, 240, 129), Common::KEYCODE_u, &_iconSprites);
- addButton(Common::Rect(250, 109, 274, 129), Common::KEYCODE_d, &_iconSprites);
- addButton(Common::Rect(284, 109, 308, 129), Common::KEYCODE_ESCAPE, &_iconSprites);
+ addButton(Common::Rect(216, 109, 240, 129), Common::KEYCODE_u, &_iconSprites, kAwardsUp);
+ addButton(Common::Rect(250, 109, 274, 129), Common::KEYCODE_d, &_iconSprites, kAwardsDown);
+ addButton(Common::Rect(284, 109, 308, 129), Common::KEYCODE_ESCAPE, &_iconSprites, kAwardsExit);
}
} // End of namespace Xeen
diff --git a/engines/mm/xeen/dialogs/dialogs_char_info.cpp b/engines/mm/xeen/dialogs/dialogs_char_info.cpp
index c30343794ef..44eeda4f18f 100644
--- a/engines/mm/xeen/dialogs/dialogs_char_info.cpp
+++ b/engines/mm/xeen/dialogs/dialogs_char_info.cpp
@@ -30,6 +30,43 @@
namespace MM {
namespace Xeen {
+#ifdef USE_TTS
+
+static const uint8 kCharacterInfoInformationCount = 20;
+static const uint8 kCharacterInfoSideButtonCount = 4;
+static const uint8 kCharacterInfoPartyGoldIndex = 7;
+static const uint8 kCharacterInfoRowCount = 5;
+static const uint8 kCharacterInfoColumnCount = 4;
+
+#endif
+
+enum CharacterInfoButtonTTSTextIndex {
+ kCharacterInfoItem = 0,
+ kCharacterInfoQuickRef = 1,
+ kCharacterInfoExchange = 2,
+ kCharacterInfoExit = 3,
+ kCharacterInfoMight = 4,
+ kCharacterInfoAccuracy = 5,
+ kCharacterInfoHP = 6,
+ kCharacterInfoExperience = 7,
+ kCharacterInfoIntellect = 8,
+ kCharacterInfoLuck = 9,
+ kCharacterInfoSP = 10,
+ kCharacterInfoPartyGold = 11,
+ kCharacterInfoPersonality = 12,
+ kCharacterInfoAge = 13,
+ kCharacterInfoResistances = 14,
+ kCharacterInfoPartyGems = 15,
+ kCharacterInfoEndurance = 16,
+ kCharacterInfoLevel = 17,
+ kCharacterInfoSkills = 18,
+ kCharacterInfoPartyFood = 19,
+ kCharacterInfoSpeed = 20,
+ kCharacterInfoAC = 21,
+ kCharacterInfoAwards = 22,
+ kCharacterInfoCondition = 23
+};
+
void CharacterInfo::show(XeenEngine *vm, int charIndex) {
CharacterInfo *dlg = new CharacterInfo(vm);
dlg->execute(charIndex);
@@ -57,7 +94,11 @@ void CharacterInfo::execute(int charIndex) {
do {
if (redrawFlag) {
Common::String charDetails = loadCharacterDetails(*c);
- w.writeString(Common::String::format(Res.CHARACTER_TEMPLATE, charDetails.c_str()));
+ Common::String ttsMessage;
+ w.writeString(Common::String::format(Res.CHARACTER_TEMPLATE, charDetails.c_str()), false, &ttsMessage);
+#ifdef USE_TTS
+ speakText(ttsMessage);
+#endif
w.drawList(_drawList, 24);
w.update();
redrawFlag = false;
@@ -103,6 +144,9 @@ void CharacterInfo::execute(int charIndex) {
showCursor(false);
--_cursorCell;
showCursor(true);
+#ifdef USE_TTS
+ _vm->sayText(_buttonTexts[_buttons[_cursorCell]._ttsIndex], Common::TextToSpeechManager::INTERRUPT);
+#endif
}
w.update();
@@ -112,6 +156,9 @@ void CharacterInfo::execute(int charIndex) {
showCursor(false);
++_cursorCell;
showCursor(true);
+#ifdef USE_TTS
+ _vm->sayText(_buttonTexts[_buttons[_cursorCell]._ttsIndex], Common::TextToSpeechManager::INTERRUPT);
+#endif
}
w.update();
@@ -121,6 +168,9 @@ void CharacterInfo::execute(int charIndex) {
showCursor(false);
_cursorCell -= 5;
showCursor(true);
+#ifdef USE_TTS
+ _vm->sayText(_buttonTexts[_buttons[_cursorCell]._ttsIndex], Common::TextToSpeechManager::INTERRUPT);
+#endif
}
w.update();
@@ -130,6 +180,9 @@ void CharacterInfo::execute(int charIndex) {
showCursor(false);
_cursorCell += 5;
showCursor(true);
+#ifdef USE_TTS
+ _vm->sayText(_buttonTexts[_buttons[_cursorCell]._ttsIndex], Common::TextToSpeechManager::INTERRUPT);
+#endif
}
w.update();
@@ -240,32 +293,32 @@ void CharacterInfo::loadDrawStructs() {
}
void CharacterInfo::addButtons() {
- addButton(Common::Rect(10, 24, 34, 44), 1001, &_iconSprites);
- addButton(Common::Rect(10, 47, 34, 67), 1002, &_iconSprites);
- addButton(Common::Rect(10, 70, 34, 90), 1003, &_iconSprites);
- addButton(Common::Rect(10, 93, 34, 113), 1004, &_iconSprites);
- addButton(Common::Rect(10, 116, 34, 136), 1005, &_iconSprites);
- addButton(Common::Rect(61, 24, 85, 44), 1006, &_iconSprites);
- addButton(Common::Rect(61, 47, 85, 67), 1007, &_iconSprites);
- addButton(Common::Rect(61, 70, 85, 90), 1008, &_iconSprites);
- addButton(Common::Rect(61, 93, 85, 113), 1009, &_iconSprites);
- addButton(Common::Rect(61, 116, 85, 136), 1010, &_iconSprites);
- addButton(Common::Rect(112, 24, 136, 44), 1011, &_iconSprites);
- addButton(Common::Rect(112, 47, 136, 67), 1012, &_iconSprites);
- addButton(Common::Rect(112, 70, 136, 90), 1013, &_iconSprites);
- addButton(Common::Rect(112, 93, 136, 113), 1014, &_iconSprites);
- addButton(Common::Rect(112, 116, 136, 136), 1015, &_iconSprites);
- addButton(Common::Rect(177, 24, 201, 44), 1016, &_iconSprites);
- addButton(Common::Rect(177, 47, 201, 67), 1017, &_iconSprites);
- addButton(Common::Rect(177, 70, 201, 90), 1018, &_iconSprites);
- addButton(Common::Rect(177, 93, 201, 113), 1019, &_iconSprites);
- addButton(Common::Rect(177, 116, 201, 136), 1020, &_iconSprites);
-
- addButton(Common::Rect(285, 11, 309, 31), Res.KeyConstants.DialogsCharInfo.KEY_ITEM, &_iconSprites);
- addButton(Common::Rect(285, 43, 309, 63), Res.KeyConstants.DialogsCharInfo.KEY_QUICK, &_iconSprites);
- addButton(Common::Rect(285, 75, 309, 95), Res.KeyConstants.DialogsCharInfo.KEY_EXCHANGE, &_iconSprites);
-
- addButton(Common::Rect(285, 107, 309, 127), Common::KEYCODE_ESCAPE, &_iconSprites);
+ addButton(Common::Rect(10, 24, 34, 44), 1001, &_iconSprites, kCharacterInfoMight);
+ addButton(Common::Rect(10, 47, 34, 67), 1002, &_iconSprites, kCharacterInfoIntellect);
+ addButton(Common::Rect(10, 70, 34, 90), 1003, &_iconSprites, kCharacterInfoPersonality);
+ addButton(Common::Rect(10, 93, 34, 113), 1004, &_iconSprites, kCharacterInfoEndurance);
+ addButton(Common::Rect(10, 116, 34, 136), 1005, &_iconSprites, kCharacterInfoSpeed);
+ addButton(Common::Rect(61, 24, 85, 44), 1006, &_iconSprites, kCharacterInfoAccuracy);
+ addButton(Common::Rect(61, 47, 85, 67), 1007, &_iconSprites, kCharacterInfoLuck);
+ addButton(Common::Rect(61, 70, 85, 90), 1008, &_iconSprites, kCharacterInfoAge);
+ addButton(Common::Rect(61, 93, 85, 113), 1009, &_iconSprites, kCharacterInfoLevel);
+ addButton(Common::Rect(61, 116, 85, 136), 1010, &_iconSprites, kCharacterInfoAC);
+ addButton(Common::Rect(112, 24, 136, 44), 1011, &_iconSprites, kCharacterInfoHP);
+ addButton(Common::Rect(112, 47, 136, 67), 1012, &_iconSprites, kCharacterInfoSP);
+ addButton(Common::Rect(112, 70, 136, 90), 1013, &_iconSprites, kCharacterInfoResistances);
+ addButton(Common::Rect(112, 93, 136, 113), 1014, &_iconSprites, kCharacterInfoSkills);
+ addButton(Common::Rect(112, 116, 136, 136), 1015, &_iconSprites, kCharacterInfoAwards);
+ addButton(Common::Rect(177, 24, 201, 44), 1016, &_iconSprites, kCharacterInfoExperience);
+ addButton(Common::Rect(177, 47, 201, 67), 1017, &_iconSprites, kCharacterInfoPartyGold);
+ addButton(Common::Rect(177, 70, 201, 90), 1018, &_iconSprites, kCharacterInfoPartyGems);
+ addButton(Common::Rect(177, 93, 201, 113), 1019, &_iconSprites, kCharacterInfoPartyFood);
+ addButton(Common::Rect(177, 116, 201, 136), 1020, &_iconSprites, kCharacterInfoCondition);
+
+ addButton(Common::Rect(285, 11, 309, 31), Res.KeyConstants.DialogsCharInfo.KEY_ITEM, &_iconSprites, kCharacterInfoItem);
+ addButton(Common::Rect(285, 43, 309, 63), Res.KeyConstants.DialogsCharInfo.KEY_QUICK, &_iconSprites, kCharacterInfoQuickRef);
+ addButton(Common::Rect(285, 75, 309, 95), Res.KeyConstants.DialogsCharInfo.KEY_EXCHANGE, &_iconSprites, kCharacterInfoExchange);
+
+ addButton(Common::Rect(285, 107, 309, 127), Common::KEYCODE_ESCAPE, &_iconSprites, kCharacterInfoExit);
addPartyButtons(_vm);
}
@@ -372,6 +425,56 @@ const char *CharacterInfo::getFoodOnHandPlurals(int food) {
return Res.FOOD_ON_HAND[0];
}
+#ifdef USE_TTS
+
+void CharacterInfo::speakText(const Common::String &text) {
+ uint index = 0;
+ uint statNameIndex = 0;
+
+ // Get the header for each piece of information
+ Common::String informationHeaders[kCharacterInfoInformationCount];
+ for (uint i = 0; i < kCharacterInfoInformationCount; ++i) {
+ if (i != kCharacterInfoPartyGoldIndex) {
+ // Items in the rightmost column already have their full names, not abbreviations
+ if ((i + 1) % kCharacterInfoColumnCount != 0) {
+ // Replace abbreviations with their full versions
+ informationHeaders[i] = Res.STAT_NAMES[statNameIndex];
+ getNextTextSection(text, index);
+ } else {
+ informationHeaders[i] = getNextTextSection(text, index);
+ }
+ }
+
+ // The text is displayed in order from left to right, while the stat names in the STAT_NAMES array are ordered
+ // from top to bottom. Therefore, we need to set the stat name index to correspond to the STAT_NAMES array
+ statNameIndex += kCharacterInfoRowCount;
+ if (statNameIndex >= kCharacterInfoInformationCount) {
+ statNameIndex -= kCharacterInfoInformationCount - 1;
+ }
+ }
+
+ // Get the text for the side buttons
+ Common::String sideButtonsText = addNextTextToButtons(text, index, kCharacterInfoSideButtonCount);
+
+ // Party gold label (but not the value) sorts out of order and not with the rest of the information,
+ // so we need to move it to its correct place
+ informationHeaders[kCharacterInfoPartyGoldIndex] = getNextTextSection(text, index);
+
+ // Character name
+ _vm->sayText(getNextTextSection(text, index), Common::TextToSpeechManager::INTERRUPT);
+
+ // Each attribute
+ for (uint i = 0; i < kCharacterInfoInformationCount; ++i) {
+ Common::String buttonText = informationHeaders[i] + ": " + getNextTextSection(text, index);
+ _vm->sayText(buttonText);
+ _buttonTexts.push_back(buttonText);
+ }
+
+ _vm->sayText(sideButtonsText);
+}
+
+#endif
+
bool CharacterInfo::expandStat(int attrib, const Character &c) {
const int STAT_POS[2][20] = {
{
@@ -620,6 +723,9 @@ bool CharacterInfo::expandStat(int attrib, const Character &c) {
while (!_vm->shouldExit() && !events.isKeyMousePressed())
events.pollEventsAndWait();
events.clearEvents();
+#ifdef USE_TTS
+ _vm->stopTextToSpeech();
+#endif
w.close();
return false;
diff --git a/engines/mm/xeen/dialogs/dialogs_char_info.h b/engines/mm/xeen/dialogs/dialogs_char_info.h
index 53129ec4199..6298f26611c 100644
--- a/engines/mm/xeen/dialogs/dialogs_char_info.h
+++ b/engines/mm/xeen/dialogs/dialogs_char_info.h
@@ -74,6 +74,10 @@ private:
*/
const char *getFoodOnHandPlurals(int food);
+#ifdef USE_TTS
+ void speakText(const Common::String &text);
+#endif
+
bool expandStat(int attrib, const Character &c);
public:
static void show(XeenEngine *vm, int charIndex);
diff --git a/engines/mm/xeen/dialogs/dialogs_control_panel.cpp b/engines/mm/xeen/dialogs/dialogs_control_panel.cpp
index a6474b008e0..8db974fbe2c 100644
--- a/engines/mm/xeen/dialogs/dialogs_control_panel.cpp
+++ b/engines/mm/xeen/dialogs/dialogs_control_panel.cpp
@@ -28,6 +28,23 @@
namespace MM {
namespace Xeen {
+#ifdef USE_TTS
+
+static const uint8 kControlPanelTextCount = 7;
+static const uint8 kControlPanelEfxIndex = 1;
+static const uint8 kControlPanelMusicIndex = 3;
+
+#endif
+
+enum ControlPanelButtonTTSTextIndex {
+ kControlPanelLoad = 0,
+ kControlPanelEfx = 1,
+ kControlPanelSave = 2,
+ kControlPanelMusic = 3,
+ kControlPanelQuit = 4,
+ kControlPanelWizard = 5
+};
+
int ControlPanel::show(XeenEngine *vm) {
ControlPanel *dlg = new ControlPanel(vm);
int result = dlg->execute();
@@ -51,24 +68,37 @@ int ControlPanel::execute() {
int result = 0, debugCtr = 0;
w.open();
+#ifdef USE_TTS
+ bool voiceText = true;
+ _vm->stopTextToSpeech();
+#endif
do {
Common::String btnText = getButtonText();
Common::String text = Common::String::format(Res.CONTROL_PANEL_TEXT, btnText.c_str());
drawButtons(&w);
- w.writeString(text);
+
+ Common::String ttsMessage;
+ w.writeString(text, false, &ttsMessage);
w.writeString("\xB""000\t000\x1");
w.update();
+#ifdef USE_TTS
+ if (voiceText) {
+ speakText(ttsMessage);
+ voiceText = false;
+ }
+#endif
+
events.updateGameCounter();
intf.draw3d(false, false);
do {
- w.writeString("\r");
+ w.writeString("\r", false);
drawButtons(&w);
- w.writeString(text);
- w.writeString("\v000\t000");
+ w.writeString(text, false);
+ w.writeString("\v000\t000", false);
w.frame();
if (_debugFlag)
@@ -131,10 +161,14 @@ int ControlPanel::execute() {
sound.setFxOn(!sound._fxOn);
if (sound._fxOn)
sound.playFX(20);
-
+#ifdef USE_TTS
+ voiceOnOffText(sound._fxOn, kControlPanelEfxIndex);
+#endif
} else if (Res.KeyConstants.DialogsControlPanel.KEY_MUSICON == _buttonValue) {
sound.setMusicOn(!sound._musicOn);
-
+#ifdef USE_TTS
+ voiceOnOffText(sound._musicOn, kControlPanelMusicIndex);
+#endif
} else if (Common::KEYCODE_ESCAPE == _buttonValue) {
result = 1;
@@ -168,17 +202,17 @@ int ControlPanel::execute() {
void ControlPanel::loadButtons() {
_iconSprites.load("cpanel.icn");
- addButton(Common::Rect(214, 56, 244, 69), Res.KeyConstants.DialogsControlPanel.KEY_FXON, 0, &_iconSprites);
- addButton(Common::Rect(214, 75, 244, 88), Res.KeyConstants.DialogsControlPanel.KEY_MUSICON, 0, &_iconSprites);
- addButton(Common::Rect(135, 56, 165, 69), Res.KeyConstants.DialogsControlPanel.KEY_LOAD, 0, &_iconSprites);
- addButton(Common::Rect(135, 75, 165, 88), Res.KeyConstants.DialogsControlPanel.KEY_SAVE, 0, &_iconSprites);
+ addButton(Common::Rect(214, 56, 244, 69), Res.KeyConstants.DialogsControlPanel.KEY_FXON, 0, &_iconSprites, kControlPanelEfx);
+ addButton(Common::Rect(214, 75, 244, 88), Res.KeyConstants.DialogsControlPanel.KEY_MUSICON, 0, &_iconSprites, kControlPanelMusic);
+ addButton(Common::Rect(135, 56, 165, 69), Res.KeyConstants.DialogsControlPanel.KEY_LOAD, 0, &_iconSprites, kControlPanelLoad);
+ addButton(Common::Rect(135, 75, 165, 88), Res.KeyConstants.DialogsControlPanel.KEY_SAVE, 0, &_iconSprites, kControlPanelSave);
// For ScummVM we've merged both Save and Save As into a single
// save item, so we don't need this one
addButton(Common::Rect(), 0);
- addButton(Common::Rect(135, 94, 165, 107), Res.KeyConstants.DialogsControlPanel.KEY_QUIT, 0, &_iconSprites);
- addButton(Common::Rect(175, 113, 205, 126), Res.KeyConstants.DialogsControlPanel.KEY_MRWIZARD, 0, &_iconSprites);
+ addButton(Common::Rect(135, 94, 165, 107), Res.KeyConstants.DialogsControlPanel.KEY_QUIT, 0, &_iconSprites, kControlPanelQuit);
+ addButton(Common::Rect(175, 113, 205, 126), Res.KeyConstants.DialogsControlPanel.KEY_MRWIZARD, 0, &_iconSprites, kControlPanelWizard);
}
Common::String ControlPanel::getButtonText() {
@@ -205,5 +239,27 @@ Common::String ControlPanel::getTimeText() const {
timeStr.c_str(), playtimeStr.c_str());
}
+#ifdef USE_TTS
+
+void ControlPanel::speakText(const Common::String &text) {
+ uint index = 0;
+ _vm->sayText(getNextTextSection(text, index, kControlPanelTextCount));
+ addNextTextToButtons(text, index, kControlPanelTextCount - 1);
+}
+
+void ControlPanel::voiceOnOffText(bool on, uint buttonTextIndex) {
+ const char *baseMessage = on ? Res.ON : Res.OFF;
+ Common::String cleanedMessage;
+ for (uint i = 0; i < strlen(baseMessage); ++i) {
+ if (Common::isAlpha(baseMessage[i])) {
+ cleanedMessage += baseMessage[i];
+ }
+ }
+ _vm->sayText(cleanedMessage, Common::TextToSpeechManager::INTERRUPT);
+ _buttonTexts[buttonTextIndex] = cleanedMessage;
+}
+
+#endif
+
} // End of namespace Xeen
} // End of namespace MM
diff --git a/engines/mm/xeen/dialogs/dialogs_control_panel.h b/engines/mm/xeen/dialogs/dialogs_control_panel.h
index fc5ca126f1a..ecc84f44f65 100644
--- a/engines/mm/xeen/dialogs/dialogs_control_panel.h
+++ b/engines/mm/xeen/dialogs/dialogs_control_panel.h
@@ -54,6 +54,21 @@ private:
* Gets the current time
*/
Common::String getTimeText() const;
+
+#ifdef USE_TTS
+ /**
+ * Voices text as text-to-speech and sets the buttons
+ * @param text Text of the control panel, each piece separated by a newline
+ */
+ void speakText(const Common::String &text);
+
+ /**
+ * Voices "On" or "Off" text and changes the TTS text of an on/off button to this text
+ * @param on Whether to voice "on" or "off"
+ * @param buttonTextIndex The index of the button text to change
+ */
+ void voiceOnOffText(bool on, uint buttonTextIndex);
+#endif
public:
/**
* Show the control panel
diff --git a/engines/mm/xeen/dialogs/dialogs_copy_protection.cpp b/engines/mm/xeen/dialogs/dialogs_copy_protection.cpp
index ba850f645a3..796f6efa7ae 100644
--- a/engines/mm/xeen/dialogs/dialogs_copy_protection.cpp
+++ b/engines/mm/xeen/dialogs/dialogs_copy_protection.cpp
@@ -27,6 +27,12 @@
namespace MM {
namespace Xeen {
+#ifdef USE_TTS
+
+static const uint8 kCopyProtectionDirectionCount = 2;
+
+#endif
+
bool CopyProtection::show(XeenEngine *vm) {
CopyProtection *dlg = new CopyProtection(vm);
int result = dlg->execute();
@@ -52,9 +58,14 @@ bool CopyProtection::execute() {
protEntry._pageNum, protEntry._lineNum, protEntry._wordNum);
w.open();
- w.writeString(msg);
+ Common::String ttsMessage;
+ w.writeString(msg, false, &ttsMessage);
w.update();
+#ifdef USE_TTS
+ speakText(ttsMessage);
+#endif
+
for (int tryNum = 0; tryNum < 3 && !_vm->shouldExit(); ++tryNum) {
line.clear();
if (getString(line, 20, 200, false) && !line.compareToIgnoreCase(protEntry._text)) {
@@ -97,5 +108,17 @@ void CopyProtection::loadEntries() {
}
}
+#ifdef USE_TTS
+
+void CopyProtection::speakText(const Common::String &text) const {
+ uint index = 0;
+ _vm->sayText(getNextTextSection(text, index), Common::TextToSpeechManager::INTERRUPT);
+ // Combine the directions so they're spoken cleanly as one sentence
+ _vm->sayText(getNextTextSection(text, index, kCopyProtectionDirectionCount, " "));
+ _vm->sayText(text.substr(index));
+}
+
+#endif
+
} // End of namespace Xeen
} // End of namespace MM
diff --git a/engines/mm/xeen/dialogs/dialogs_copy_protection.h b/engines/mm/xeen/dialogs/dialogs_copy_protection.h
index 7c3d97ac6f7..92901ce3ca8 100644
--- a/engines/mm/xeen/dialogs/dialogs_copy_protection.h
+++ b/engines/mm/xeen/dialogs/dialogs_copy_protection.h
@@ -52,6 +52,14 @@ private:
* Load the copy protection entries
*/
void loadEntries();
+
+#ifdef USE_TTS
+ /**
+ * Voices text with TTS
+ * @param text Text to voice
+ */
+ void speakText(const Common::String &text) const;
+#endif
public:
/**
* Show the dialog
diff --git a/engines/mm/xeen/dialogs/dialogs_create_char.cpp b/engines/mm/xeen/dialogs/dialogs_create_char.cpp
index f4eaf8c9e7f..6f1408733a9 100644
--- a/engines/mm/xeen/dialogs/dialogs_create_char.cpp
+++ b/engines/mm/xeen/dialogs/dialogs_create_char.cpp
@@ -26,6 +26,43 @@
namespace MM {
namespace Xeen {
+#ifdef USE_TTS
+
+static const uint8 kCreateCharacterBasicInfoCount = 6;
+static const uint8 kCreateCharacterSideButtonCount = 3;
+
+#endif
+
+enum CreateCharacterButtonTTSTextIndex {
+ kCreateCharacterRoll = 0,
+ kCreateCharacterCreate = 1,
+ kCreateCharacterExit = 2,
+ kCreateCharacterMight = 3,
+ kCreateCharacterIntellect = 4,
+ kCreateCharacterPersonality = 5,
+ kCreateCharacterEndurance = 6,
+ kCreateCharacterSpeed = 7,
+ kCreateCharacterAccuracy = 8,
+ kCreateCharacterLuck = 9,
+ kCreateCharacterKnight = 10,
+ kCreateCharacterPaladin = 11,
+ kCreateCharacterArcher = 12,
+ kCreateCharacterCleric = 13,
+ kCreateCharacterSorcerer = 14,
+ kCreateCharacterRobber = 15,
+ kCreateCharacterNinja = 16,
+ kCreateCharacterBarbarian = 17,
+ kCreateCharacterDruid = 18,
+ kCreateCharacterRanger = 19,
+ kCreateCharacterSwapMight = 20,
+ kCreateCharacterSwapIntellect = 21,
+ kCreateCharacterSwapPersonality = 22,
+ kCreateCharacterSwapEndurance = 23,
+ kCreateCharacterSwapSpeed = 24,
+ kCreateCharacterSwapAccuracy = 25,
+ kCreateCharacterSwapLuck = 26
+};
+
void CreateCharacterDialog::show(XeenEngine *vm) {
CreateCharacterDialog *dlg = new CreateCharacterDialog(vm);
dlg->execute();
@@ -104,9 +141,13 @@ void CreateCharacterDialog::execute() {
party._roster[freeCharList[charIndex]]._faceSprites->draw(
w, 0, Common::Point(27, 102));
+ Common::String ttsMessage;
// Render all on-screen text
- w.writeString(msg);
+ w.writeString(msg, false, &ttsMessage);
w.update();
+#ifdef USE_TTS
+ speakText(ttsMessage, false, classId != -1, selectedClass);
+#endif
// Draw the arrow for the selected class, if applicable
if (selectedClass != -1)
@@ -221,8 +262,12 @@ void CreateCharacterDialog::execute() {
party._roster[freeCharList[charIndex]]._faceSprites->draw(w, 0,
Common::Point(27, 102));
- w.writeString(msg);
+ Common::String ttsMessage;
+ w.writeString(msg, false, &ttsMessage);
w.update();
+#ifdef USE_TTS
+ speakText(ttsMessage, true, classId != -1, selectedClass);
+#endif
if (selectedClass != -1) {
printSelectionArrow(selectedClass);
@@ -249,31 +294,31 @@ void CreateCharacterDialog::loadButtons() {
_icons.load("create.icn");
// Add buttons
- addButton(Common::Rect(132, 98, 156, 118), Res.KeyConstants.DialogsCreateChar.KEY_ROLL, &_icons);
- addButton(Common::Rect(132, 128, 156, 148), Res.KeyConstants.DialogsCreateChar.KEY_CREATE, &_icons);
+ addButton(Common::Rect(132, 98, 156, 118), Res.KeyConstants.DialogsCreateChar.KEY_ROLL, &_icons, kCreateCharacterRoll);
+ addButton(Common::Rect(132, 128, 156, 148), Res.KeyConstants.DialogsCreateChar.KEY_CREATE, &_icons, kCreateCharacterCreate);
- addButton(Common::Rect(132, 158, 156, 178), Common::KEYCODE_ESCAPE, &_icons);
+ addButton(Common::Rect(132, 158, 156, 178), Common::KEYCODE_ESCAPE, &_icons, kCreateCharacterExit);
addButton(Common::Rect(86, 98, 110, 118), Common::KEYCODE_UP, &_icons);
addButton(Common::Rect(86, 120, 110, 140), Common::KEYCODE_DOWN, &_icons);
- addButton(Common::Rect(168, 19, 192, 39), Res.KeyConstants.DialogsCreateChar.KEY_MGT, nullptr);
- addButton(Common::Rect(168, 43, 192, 63), Res.KeyConstants.DialogsCreateChar.KEY_INT, nullptr);
- addButton(Common::Rect(168, 67, 192, 87), Res.KeyConstants.DialogsCreateChar.KEY_PER, nullptr);
- addButton(Common::Rect(168, 91, 192, 111), Res.KeyConstants.DialogsCreateChar.KEY_END, nullptr);
- addButton(Common::Rect(168, 115, 192, 135), Res.KeyConstants.DialogsCreateChar.KEY_SPD, nullptr);
- addButton(Common::Rect(168, 139, 192, 159), Res.KeyConstants.DialogsCreateChar.KEY_ACY, nullptr);
- addButton(Common::Rect(168, 163, 192, 183), Res.KeyConstants.DialogsCreateChar.KEY_LCK, nullptr);
-
- addButton(Common::Rect(227, 19, 239, 29), 1000, nullptr);
- addButton(Common::Rect(227, 30, 239, 40), 1001, nullptr);
- addButton(Common::Rect(227, 41, 239, 51), 1002, nullptr);
- addButton(Common::Rect(227, 52, 239, 62), 1003, nullptr);
- addButton(Common::Rect(227, 63, 239, 73), 1004, nullptr);
- addButton(Common::Rect(227, 74, 239, 84), 1005, nullptr);
- addButton(Common::Rect(227, 85, 239, 95), 1006, nullptr);
- addButton(Common::Rect(227, 96, 239, 106), 1007, nullptr);
- addButton(Common::Rect(227, 107, 239, 117), 1008, nullptr);
- addButton(Common::Rect(227, 118, 239, 128), 1009, nullptr);
+ addButton(Common::Rect(168, 19, 192, 39), Res.KeyConstants.DialogsCreateChar.KEY_MGT, nullptr, kCreateCharacterMight);
+ addButton(Common::Rect(168, 43, 192, 63), Res.KeyConstants.DialogsCreateChar.KEY_INT, nullptr, kCreateCharacterIntellect);
+ addButton(Common::Rect(168, 67, 192, 87), Res.KeyConstants.DialogsCreateChar.KEY_PER, nullptr, kCreateCharacterPersonality);
+ addButton(Common::Rect(168, 91, 192, 111), Res.KeyConstants.DialogsCreateChar.KEY_END, nullptr, kCreateCharacterEndurance);
+ addButton(Common::Rect(168, 115, 192, 135), Res.KeyConstants.DialogsCreateChar.KEY_SPD, nullptr, kCreateCharacterSpeed);
+ addButton(Common::Rect(168, 139, 192, 159), Res.KeyConstants.DialogsCreateChar.KEY_ACY, nullptr, kCreateCharacterAccuracy);
+ addButton(Common::Rect(168, 163, 192, 183), Res.KeyConstants.DialogsCreateChar.KEY_LCK, nullptr, kCreateCharacterLuck);
+
+ addButton(Common::Rect(227, 19, 239, 29), 1000, nullptr, kCreateCharacterKnight);
+ addButton(Common::Rect(227, 30, 239, 40), 1001, nullptr, kCreateCharacterPaladin);
+ addButton(Common::Rect(227, 41, 239, 51), 1002, nullptr, kCreateCharacterArcher);
+ addButton(Common::Rect(227, 52, 239, 62), 1003, nullptr, kCreateCharacterCleric);
+ addButton(Common::Rect(227, 63, 239, 73), 1004, nullptr, kCreateCharacterSorcerer);
+ addButton(Common::Rect(227, 74, 239, 84), 1005, nullptr, kCreateCharacterRobber);
+ addButton(Common::Rect(227, 85, 239, 95), 1006, nullptr, kCreateCharacterNinja);
+ addButton(Common::Rect(227, 96, 239, 106), 1007, nullptr, kCreateCharacterBarbarian);
+ addButton(Common::Rect(227, 107, 239, 117), 1008, nullptr, kCreateCharacterDruid);
+ addButton(Common::Rect(227, 118, 239, 128), 1009, nullptr, kCreateCharacterRanger);
}
void CreateCharacterDialog::drawIcons() {
@@ -523,13 +568,21 @@ int CreateCharacterDialog::exchangeAttribute(int srcAttr) {
saveButtons();
addButton(Common::Rect(118, 58, 142, 78), Common::KEYCODE_ESCAPE, &_icons);
- addButton(Common::Rect(168, 19, 192, 39), Res.KeyConstants.DialogsCreateChar.KEY_MGT);
- addButton(Common::Rect(168, 43, 192, 63), Res.KeyConstants.DialogsCreateChar.KEY_INT);
- addButton(Common::Rect(168, 67, 192, 87), Res.KeyConstants.DialogsCreateChar.KEY_PER);
- addButton(Common::Rect(168, 91, 192, 111), Res.KeyConstants.DialogsCreateChar.KEY_END);
- addButton(Common::Rect(168, 115, 192, 135), Res.KeyConstants.DialogsCreateChar.KEY_SPD);
- addButton(Common::Rect(168, 139, 192, 159), Res.KeyConstants.DialogsCreateChar.KEY_ACY);
- addButton(Common::Rect(168, 163, 192, 183), Res.KeyConstants.DialogsCreateChar.KEY_LCK);
+ addButton(Common::Rect(168, 19, 192, 39), Res.KeyConstants.DialogsCreateChar.KEY_MGT, nullptr, kCreateCharacterSwapMight);
+ addButton(Common::Rect(168, 43, 192, 63), Res.KeyConstants.DialogsCreateChar.KEY_INT, nullptr, kCreateCharacterSwapIntellect);
+ addButton(Common::Rect(168, 67, 192, 87), Res.KeyConstants.DialogsCreateChar.KEY_PER, nullptr, kCreateCharacterSwapPersonality);
+ addButton(Common::Rect(168, 91, 192, 111), Res.KeyConstants.DialogsCreateChar.KEY_END, nullptr, kCreateCharacterSwapEndurance);
+ addButton(Common::Rect(168, 115, 192, 135), Res.KeyConstants.DialogsCreateChar.KEY_SPD, nullptr, kCreateCharacterSwapSpeed);
+ addButton(Common::Rect(168, 139, 192, 159), Res.KeyConstants.DialogsCreateChar.KEY_ACY, nullptr, kCreateCharacterSwapAccuracy);
+ addButton(Common::Rect(168, 163, 192, 183), Res.KeyConstants.DialogsCreateChar.KEY_LCK, nullptr, kCreateCharacterSwapLuck);
+
+#ifdef USE_TTS
+ for (uint i = 0; i < TOTAL_ATTRIBUTES; ++i) {
+ _buttonTexts.push_back(Res.STAT_NAMES[i]);
+ }
+
+ _vm->stopTextToSpeech();
+#endif
Window &w = windows[26];
w.open();
@@ -558,6 +611,11 @@ int CreateCharacterDialog::exchangeAttribute(int srcAttr) {
w.close();
restoreButtons();
+#ifdef USE_TTS
+ for (uint i = 0; i < TOTAL_ATTRIBUTES; ++i) {
+ _buttonTexts.pop_back();
+ }
+#endif
_buttonValue = 0;
return result;
}
@@ -588,6 +646,9 @@ bool CreateCharacterDialog::saveCharacter(Character &c, int classId, Race race,
// Name aborted, so exit
return false;
+#ifdef USE_TTS
+ _vm->sayText(name, Common::TextToSpeechManager::INTERRUPT);
+#endif
// Save new character details
c.clear();
c._name = name;
@@ -640,5 +701,91 @@ bool CreateCharacterDialog::saveCharacter(Character &c, int classId, Race race,
return true;
}
+#ifdef USE_TTS
+
+void CreateCharacterDialog::speakText(const Common::String &text, bool hasAttributeLabels, bool classSelected, int selectedClass) {
+ _vm->stopTextToSpeech();
+ uint index = 0;
+ bool addNewButtons = _buttonTexts.empty();
+
+ Common::String buttonTexts;
+ if (!hasAttributeLabels) {
+ // Roll/create/ESC buttons
+ if (addNewButtons) {
+ buttonTexts = addNextTextToButtons(text, index, kCreateCharacterSideButtonCount);
+ } else {
+ buttonTexts = getNextTextSection(text, index, kCreateCharacterSideButtonCount);
+ }
+
+ for (uint i = 0; i < TOTAL_ATTRIBUTES; ++i) {
+ getNextTextSection(text, index);
+ }
+ }
+
+ // Race/sex/class info
+ _vm->sayText(getNextTextSection(text, index, kCreateCharacterBasicInfoCount));
+
+ uint classIndex = 0;
+
+ // The selected class is at the very end of the string, but it should be voiced with the rest of the race/sex/class
+ // info, so find it here early and voice it
+ if (classSelected) {
+ uint endClassIndex = text.findLastNotOf('\n');
+
+ if (endClassIndex != Common::String::npos) {
+ for (uint i = endClassIndex; i >= index; --i) {
+ if (text[i] == '\n') {
+ classIndex = i;
+ _vm->sayText(text.substr(classIndex));
+ break;
+ }
+ }
+ }
+ }
+
+ if (!hasAttributeLabels) {
+ _vm->sayText(buttonTexts);
+ }
+
+ // Get attribute values
+ for (uint i = 0; i < TOTAL_ATTRIBUTES; ++i) {
+ Common::String attribute = Common::String(Res.STAT_NAMES[i]) + ": " + getNextTextSection(text, index);
+
+ if (addNewButtons) {
+ _buttonTexts.push_back(attribute);
+ } else {
+ _buttonTexts[i + kCreateCharacterSideButtonCount] = attribute;
+ }
+
+ _vm->sayText(attribute);
+ }
+
+ // Classes
+ for (int i = 0; i < TOTAL_CLASSES; ++i) {
+ Common::String buttonText;
+ if (_allowedClasses[i]) {
+ buttonText = getNextTextSection(text, index);
+ } else {
+ buttonText = "";
+ getNextTextSection(text, index);
+ }
+
+ if (addNewButtons) {
+ _buttonTexts.push_back(buttonText);
+ } else {
+ _buttonTexts[i + TOTAL_CLASSES] = buttonText;
+ }
+
+ if (i == selectedClass) {
+ _vm->sayText(buttonText);
+ }
+ }
+
+ // Skills
+ _vm->sayText(text.substr(index, classIndex - index));
+}
+
+#endif
+
} // End of namespace Xeen
} // End of namespace MM
diff --git a/engines/mm/xeen/dialogs/dialogs_create_char.h b/engines/mm/xeen/dialogs/dialogs_create_char.h
index 4b9d04d6b0a..c2590caa8a4 100644
--- a/engines/mm/xeen/dialogs/dialogs_create_char.h
+++ b/engines/mm/xeen/dialogs/dialogs_create_char.h
@@ -112,6 +112,17 @@ private:
* well as a list of classes that the attributes meet the requirements for
*/
void rollAttributes();
+
+#ifdef USE_TTS
+ /**
+ * Voices text with TTS and sets up buttons
+ * @param text Text for voicing and buttons. Each section should be separated by a newline
+ * @param hasAttributeLabels Whether this text has attribute labels in it
+ * @param classSelected Whether a class is selected
+ * @param selectedClass Class selected by the player
+ */
+ void speakText(const Common::String &text, bool hasAttributeLabels, bool classSelected, int selectedClass);
+#endif
public:
/**
* Shows the Create Character dialog
diff --git a/engines/mm/xeen/dialogs/dialogs_difficulty.cpp b/engines/mm/xeen/dialogs/dialogs_difficulty.cpp
index b3f8a2758d5..9ebf7f3733b 100644
--- a/engines/mm/xeen/dialogs/dialogs_difficulty.cpp
+++ b/engines/mm/xeen/dialogs/dialogs_difficulty.cpp
@@ -26,6 +26,24 @@
namespace MM {
namespace Xeen {
+#ifdef USE_TTS
+
+static const char *difficultyButtonsText[] = {
+ "Adventurer\nWarrior", // English
+ "Abenteurer\nK\204mpfer", // German
+ "Aventurier\nGuerrier", // French
+ "Aventurero\nGuerrero", // Spanish
+ "\x80\xa2\xa0\xad\xe2\xee\xe0\xa8\xe1\n\x82\xae\xa8\xad", // Russian (ÐванÑÑÑиÑ, Ðоин)
+ "\xab\x5f\xc0\x49\xbc\xd2\xa6\xa1\n\xbe\xd4\xa4\x68\xbc\xd2\xa6\xa1" // Chinese (åéªæ¨¡å¼, æ°å£«æ¨¡å¼)
+};
+
+#endif
+
+enum DifficultyButtonTTSTextIndex {
+ kDifficultyAdventurer = 0,
+ kDifficultyWarrior = 1
+};
+
int DifficultyDialog::show(XeenEngine *vm) {
DifficultyDialog *dlg = new DifficultyDialog(vm);
int result = dlg->execute();
@@ -36,6 +54,9 @@ int DifficultyDialog::show(XeenEngine *vm) {
DifficultyDialog::DifficultyDialog(XeenEngine *vm) : ButtonContainer(vm) {
loadButtons();
+#ifdef USE_TTS
+ setButtonTexts(difficultyButtonsText[_vm->_ttsLanguage]);
+#endif
}
int DifficultyDialog::execute() {
@@ -44,6 +65,9 @@ int DifficultyDialog::execute() {
Window &w = windows[6];
w.open();
+#ifdef USE_TTS
+ _vm->stopTextToSpeech();
+#endif
w.writeString(Res.DIFFICULTY_TEXT);
drawButtons(&w);
@@ -69,8 +93,8 @@ int DifficultyDialog::execute() {
void DifficultyDialog::loadButtons() {
_sprites.load("choice.icn");
- addButton(Common::Rect(68, 167, 158, 187), Res.KeyConstants.DialogsDifficulty.KEY_ADVENTURER, &_sprites);
- addButton(Common::Rect(166, 167, 256, 187), Res.KeyConstants.DialogsDifficulty.KEY_WARRIOR, &_sprites);
+ addButton(Common::Rect(68, 167, 158, 187), Res.KeyConstants.DialogsDifficulty.KEY_ADVENTURER, &_sprites, kDifficultyAdventurer);
+ addButton(Common::Rect(166, 167, 256, 187), Res.KeyConstants.DialogsDifficulty.KEY_WARRIOR, &_sprites, kDifficultyWarrior);
}
} // End of namespace Xeen
diff --git a/engines/mm/xeen/dialogs/dialogs_dismiss.cpp b/engines/mm/xeen/dialogs/dialogs_dismiss.cpp
index c83cd533a93..9c6c07f0db7 100644
--- a/engines/mm/xeen/dialogs/dialogs_dismiss.cpp
+++ b/engines/mm/xeen/dialogs/dialogs_dismiss.cpp
@@ -43,6 +43,7 @@ void Dismiss::execute() {
Window &w = windows[31];
w.open();
+ bool ttsVoiceText = true;
bool breakFlag = false;
while (!_vm->shouldExit() && !breakFlag) {
do {
@@ -51,9 +52,10 @@ void Dismiss::execute() {
w.frame();
w.fill();
- w.writeString(Res.DISMISS_WHOM);
+ w.writeString(Res.DISMISS_WHOM, ttsVoiceText);
_iconSprites.draw(w, 0, Common::Point(225, 120));
w.update();
+ ttsVoiceText = false;
do {
events.pollEventsAndWait();
diff --git a/engines/mm/xeen/dialogs/dialogs_info.cpp b/engines/mm/xeen/dialogs/dialogs_info.cpp
index 1408bd99f58..95979ba2165 100644
--- a/engines/mm/xeen/dialogs/dialogs_info.cpp
+++ b/engines/mm/xeen/dialogs/dialogs_info.cpp
@@ -26,6 +26,12 @@
namespace MM {
namespace Xeen {
+#ifdef USE_TTS
+
+static const uint8 kInfoTimeCount = 3;
+
+#endif
+
void InfoDialog::show(XeenEngine *vm) {
InfoDialog *dlg = new InfoDialog(vm);
dlg->execute();
@@ -64,22 +70,55 @@ void InfoDialog::execute() {
Window &w = windows[28];
w.setBounds(Common::Rect(88, 20, 248, 112 + (_lines.empty() ? 0 : _lines.size() * 9 + 13)));
w.open();
- w.writeString(details);
+ Common::String ttsMessage;
+ w.writeString(details, false, &ttsMessage);
+#ifdef USE_TTS
+ _vm->stopTextToSpeech();
+ speakText(ttsMessage);
+#endif
do {
events.updateGameCounter();
intf.draw3d(false, false);
w.frame();
- w.writeString(details);
+ w.writeString(details, false);
w.update();
events.wait(1);
} while (!_vm->shouldExit() && !events.isKeyMousePressed());
+#ifdef USE_TTS
+ _vm->stopTextToSpeech();
+#endif
+
events.clearEvents();
w.close();
}
+#ifdef USE_TTS
+
+void InfoDialog::speakText(const Common::String &text) const {
+ uint index = 0;
+
+ _vm->sayText(getNextTextSection(text, index, kInfoTimeCount));
+
+ Common::String timeInfo[kInfoTimeCount];
+ // Time, day, and year headers
+ for (uint i = 0; i < kInfoTimeCount; ++i) {
+ timeInfo[i] = getNextTextSection(text, index);
+ }
+
+ // Time, day, and year numbers
+ for (uint i = 0; i < kInfoTimeCount; ++i) {
+ _vm->sayText(timeInfo[i] + ": " + getNextTextSection(text, index));
+ }
+
+ // Any remaining lines
+ _vm->sayText(text.substr(index));
+}
+
+#endif
+
void InfoDialog::protectionText() {
Party &party = *_vm->_party;
// Common::StringArray _lines;
diff --git a/engines/mm/xeen/dialogs/dialogs_info.h b/engines/mm/xeen/dialogs/dialogs_info.h
index f1e38ea68a3..f770ea7eeab 100644
--- a/engines/mm/xeen/dialogs/dialogs_info.h
+++ b/engines/mm/xeen/dialogs/dialogs_info.h
@@ -37,6 +37,10 @@ private:
void execute();
void protectionText();
+
+#ifdef USE_TTS
+ void speakText(const Common::String &text) const;
+#endif
public:
static void show(XeenEngine *vm);
};
diff --git a/engines/mm/xeen/dialogs/dialogs_input.cpp b/engines/mm/xeen/dialogs/dialogs_input.cpp
index 13e3c9956d9..750eea2d7d0 100644
--- a/engines/mm/xeen/dialogs/dialogs_input.cpp
+++ b/engines/mm/xeen/dialogs/dialogs_input.cpp
@@ -83,7 +83,7 @@ int Input::getString(Common::String &line, uint maxLen, int maxWidth, bool isNum
_vm->_noDirectionSense = true;
Common::String msg = Common::String::format("\x3""l\t000\x4%03d\x3""c", maxWidth);
- _window->writeString(msg);
+ _window->writeString(msg, false);
_window->update();
while (!_vm->shouldExit()) {
@@ -135,11 +135,14 @@ int Input::getString(Common::String &line, uint maxLen, int maxWidth, bool isNum
if (refresh) {
msg = Common::String::format("\x3""l\t000\x4%03d\x3""c%s", maxWidth, line.c_str());
- _window->writeString(msg);
+ _window->writeString(msg, false);
_window->update();
}
}
+#ifdef USE_TTS
+ _vm->sayText(line, Common::TextToSpeechManager::INTERRUPT);
+#endif
_vm->_noDirectionSense = false;
return line.size();
}
@@ -163,7 +166,7 @@ Common::KeyState Input::waitForKey(const Common::String &msg) {
if (flag)
intf.draw3d(false);
- _window->writeString(msg);
+ _window->writeString(msg, false);
animateCursor();
_window->update();
@@ -175,7 +178,7 @@ Common::KeyState Input::waitForKey(const Common::String &msg) {
break;
}
- _window->writeString("");
+ _window->writeString("", false);
_window->update();
intf._tillMove = oldTillMove;
diff --git a/engines/mm/xeen/dialogs/dialogs_items.cpp b/engines/mm/xeen/dialogs/dialogs_items.cpp
index 615903b4e4d..10faa78d2fe 100644
--- a/engines/mm/xeen/dialogs/dialogs_items.cpp
+++ b/engines/mm/xeen/dialogs/dialogs_items.cpp
@@ -28,6 +28,58 @@
namespace MM {
namespace Xeen {
+enum ItemsButtonEnchantTTSTextIndex {
+ kItemsEnchantWeapons = 0,
+ kItemsEnchantArmor = 1,
+ kItemsEnchantAccessories = 2,
+ kItemsEnchantMisc = 3,
+ kItemsEnchantEnchant = 4,
+ kItemsEnchantExit = 5,
+ kItemsEnchantUse = 6,
+ kItemsEnchantItem1 = 7,
+ kItemsEnchantItem2 = 8,
+ kItemsEnchantItem3 = 9,
+ kItemsEnchantItem4 = 10,
+ kItemsEnchantItem5 = 11,
+ kItemsEnchantItem6 = 12,
+ kItemsEnchantItem7 = 13,
+ kItemsEnchantItem8 = 14,
+ kItemsEnchantItem9 = 15
+};
+
+enum kItemsButtonTTSTextIndex {
+ kItemsWeapons = 0,
+ kItemsArmor = 1,
+ kItemsAccessories = 2,
+ kItemsMisc = 3,
+ kItemsBuyOrEquip = 4,
+ kItemsSellOrRemove = 5,
+ kItemsIdentifyOrDiscard = 6,
+ kItemsFixOrQuest = 7,
+ kItemsExit = 8,
+ kItemsItem1 = 9,
+ kItemsItem2 = 10,
+ kItemsItem3 = 11,
+ kItemsItem4 = 12,
+ kItemsItem5 = 13,
+ kItemsItem6 = 14,
+ kItemsItem7 = 15,
+ kItemsItem8 = 16,
+ kItemsItem9 = 17
+};
+
+enum ItemSelectionButtonTTSTextIndex {
+ kItemSelectionItem1 = 0,
+ kItemSelectionItem2 = 1,
+ kItemSelectionItem3 = 2,
+ kItemSelectionItem4 = 3,
+ kItemSelectionItem5 = 4,
+ kItemSelectionItem6 = 5,
+ kItemSelectionItem7 = 6,
+ kItemSelectionItem8 = 7,
+ kItemSelectionItem9 = 8
+};
+
Character *ItemsDialog::show(XeenEngine *vm, Character *c, ItemsMode mode) {
ItemsDialog *dlg = new ItemsDialog(vm);
Character *result = dlg->execute(c, mode);
@@ -70,6 +122,11 @@ Character *ItemsDialog::execute(Character *c, ItemsMode mode) {
windows[29].open();
windows[30].open();
+ Common::String buttonsText;
+#ifdef USE_TTS
+ uint buttonTextCount = 0;
+#endif
+
enum { REDRAW_NONE, REDRAW_TEXT, REDRAW_FULL } redrawFlag = REDRAW_FULL;
for (;;) {
if (redrawFlag == REDRAW_FULL) {
@@ -91,7 +148,8 @@ Character *ItemsDialog::execute(Character *c, ItemsMode mode) {
msg = Common::String::format(Res.ITEMS_DIALOG_TEXT2, Res.BTN_GOLD);
}
- windows[29].writeString(msg);
+ buttonsText.clear();
+ windows[29].writeString(msg, false, &buttonsText);
Common::fill(&arr[0], &arr[40], 0);
itemIndex = -1;
@@ -101,6 +159,10 @@ Character *ItemsDialog::execute(Character *c, ItemsMode mode) {
if (mode != priorMode) {
// Set up the buttons for the dialog
loadButtons(mode, c, category);
+#ifdef USE_TTS
+ setButtonTexts(buttonsText);
+ buttonTextCount = _buttonTexts.size();
+#endif
priorMode = mode;
drawButtons(&windows[0]);
}
@@ -176,9 +238,19 @@ Character *ItemsDialog::execute(Character *c, ItemsMode mode) {
break;
}
}
+#ifdef USE_TTS
+ uint8 lineCount = lines.size();
+ // Make space for spells
+ _buttonTexts.resize(lines.size() + buttonTextCount - 1);
+#endif
+
while (lines.size() < INV_ITEMS_TOTAL)
lines.push_back("");
+ Common::String ttsMessage;
+#ifdef USE_TTS
+ uint8 headerCount = 0;
+#endif
// Draw out overall text and the list of items
switch (mode) {
case ITEMMODE_CHAR_INFO:
@@ -190,7 +262,11 @@ Character *ItemsDialog::execute(Character *c, ItemsMode mode) {
lines[0].c_str(), lines[1].c_str(), lines[2].c_str(), lines[3].c_str(),
lines[4].c_str(), lines[5].c_str(), lines[6].c_str(), lines[7].c_str(),
lines[8].c_str()
- ));
+ ), false, &ttsMessage);
+#ifdef USE_TTS
+ // Misc category lists charges as well
+ headerCount = category == CATEGORY_MISC ? 2 : 1;
+#endif
break;
case ITEMMODE_BUY:
@@ -199,7 +275,10 @@ Character *ItemsDialog::execute(Character *c, ItemsMode mode) {
lines[0].c_str(), lines[1].c_str(), lines[2].c_str(), lines[3].c_str(),
lines[4].c_str(), lines[5].c_str(), lines[6].c_str(), lines[7].c_str(),
lines[8].c_str()
- ));
+ ), false, &ttsMessage);
+#ifdef USE_TTS
+ headerCount = 3;
+#endif
break;
case ITEMMODE_SELL:
@@ -214,7 +293,10 @@ Character *ItemsDialog::execute(Character *c, ItemsMode mode) {
lines[0].c_str(), lines[1].c_str(), lines[2].c_str(), lines[3].c_str(),
lines[4].c_str(), lines[5].c_str(), lines[6].c_str(), lines[7].c_str(),
lines[8].c_str()
- ));
+ ), false, &ttsMessage);
+#ifdef USE_TTS
+ headerCount = 2;
+#endif
break;
case ITEMMODE_3:
@@ -224,13 +306,26 @@ Character *ItemsDialog::execute(Character *c, ItemsMode mode) {
lines[0].c_str(), lines[1].c_str(), lines[2].c_str(), lines[3].c_str(),
lines[4].c_str(), lines[5].c_str(), lines[6].c_str(), lines[7].c_str(),
lines[8].c_str()
- ));
+ ), false, &ttsMessage);
+#ifdef USE_TTS
+ headerCount = 2;
+#endif
break;
default:
break;
}
+#ifdef USE_TTS
+ if (lines[0] == Res.NO_ITEMS_AVAILABLE) {
+ // Speak headers and the "no items" message
+ uint index = 0;
+ _vm->sayText(getNextTextSection(ttsMessage, index, headerCount + 1));
+ } else {
+ speakText(ttsMessage, headerCount, lineCount);
+ }
+#endif
+
// Draw the glyphs for the items
windows[0].drawList(_itemsDrawList, INV_ITEMS_TOTAL);
windows[0].update();
@@ -278,6 +373,10 @@ Character *ItemsDialog::execute(Character *c, ItemsMode mode) {
continue;
}
+#ifdef USE_TTS
+ _vm->sayText(buttonsText);
+#endif
+
// Wait for a selection
_buttonValue = 0;
while (!_vm->shouldExit() && !_buttonValue) {
@@ -307,6 +406,9 @@ Character *ItemsDialog::execute(Character *c, ItemsMode mode) {
if (_buttonValue < (int)(_vm->_mode == MODE_COMBAT ? combat._combatParty.size() : party._activeParty.size())) {
// Character number is valid
+#ifdef USE_TTS
+ _vm->stopTextToSpeech();
+#endif
redrawFlag = REDRAW_FULL;
Character *newChar = _vm->_mode == MODE_COMBAT ? combat._combatParty[_buttonValue] : &party._activeParty[_buttonValue];
@@ -477,43 +579,43 @@ void ItemsDialog::loadButtons(ItemsMode mode, Character *&c, ItemCategory catego
clearButtons();
if (mode == ITEMMODE_ENCHANT || mode == ITEMMODE_RECHARGE || mode == ITEMMODE_TO_GOLD) {
// Enchant button list
- addButton(Common::Rect(12, 109, 36, 129), Res.KeyConstants.DialogsItems.KEY_WEAPONS, &_iconSprites);
- addButton(Common::Rect(46, 109, 70, 129), Res.KeyConstants.DialogsItems.KEY_ARMOR, &_iconSprites);
- addButton(Common::Rect(80, 109, 104, 129), Res.KeyConstants.DialogsItems.KEY_ACCESSORY, &_iconSprites);
- addButton(Common::Rect(114, 109, 138, 129), Res.KeyConstants.DialogsItems.KEY_MISC, &_iconSprites);
- addButton(Common::Rect(148, 109, 172, 129), Res.KeyConstants.DialogsItems.KEY_ENCHANT, &_iconSprites);
- addButton(Common::Rect(284, 109, 308, 129), Common::KEYCODE_ESCAPE, &_iconSprites);
- addButton(Common::Rect(148, 109, 172, 129), Res.KeyConstants.DialogsItems.KEY_USE, &_iconSprites);
- addButton(Common::Rect(8, 20, 263, 28), Common::KEYCODE_1);
- addButton(Common::Rect(8, 29, 263, 37), Common::KEYCODE_2);
- addButton(Common::Rect(8, 38, 263, 46), Common::KEYCODE_3);
- addButton(Common::Rect(8, 47, 263, 55), Common::KEYCODE_4);
- addButton(Common::Rect(8, 56, 263, 64), Common::KEYCODE_5);
- addButton(Common::Rect(8, 65, 263, 73), Common::KEYCODE_6);
- addButton(Common::Rect(8, 74, 263, 82), Common::KEYCODE_7);
- addButton(Common::Rect(8, 83, 263, 91), Common::KEYCODE_8);
- addButton(Common::Rect(8, 92, 263, 100), Common::KEYCODE_9);
+ addButton(Common::Rect(12, 109, 36, 129), Res.KeyConstants.DialogsItems.KEY_WEAPONS, &_iconSprites, kItemsEnchantWeapons);
+ addButton(Common::Rect(46, 109, 70, 129), Res.KeyConstants.DialogsItems.KEY_ARMOR, &_iconSprites, kItemsEnchantArmor);
+ addButton(Common::Rect(80, 109, 104, 129), Res.KeyConstants.DialogsItems.KEY_ACCESSORY, &_iconSprites, kItemsEnchantAccessories);
+ addButton(Common::Rect(114, 109, 138, 129), Res.KeyConstants.DialogsItems.KEY_MISC, &_iconSprites, kItemsEnchantMisc);
+ addButton(Common::Rect(148, 109, 172, 129), Res.KeyConstants.DialogsItems.KEY_ENCHANT, &_iconSprites, kItemsEnchantEnchant);
+ addButton(Common::Rect(284, 109, 308, 129), Common::KEYCODE_ESCAPE, &_iconSprites, kItemsEnchantExit);
+ addButton(Common::Rect(148, 109, 172, 129), Res.KeyConstants.DialogsItems.KEY_USE, &_iconSprites, kItemsEnchantUse);
+ addButton(Common::Rect(8, 20, 263, 28), Common::KEYCODE_1, nullptr, kItemsEnchantItem1);
+ addButton(Common::Rect(8, 29, 263, 37), Common::KEYCODE_2, nullptr, kItemsEnchantItem2);
+ addButton(Common::Rect(8, 38, 263, 46), Common::KEYCODE_3, nullptr, kItemsEnchantItem3);
+ addButton(Common::Rect(8, 47, 263, 55), Common::KEYCODE_4, nullptr, kItemsEnchantItem4);
+ addButton(Common::Rect(8, 56, 263, 64), Common::KEYCODE_5, nullptr, kItemsEnchantItem5);
+ addButton(Common::Rect(8, 65, 263, 73), Common::KEYCODE_6, nullptr, kItemsEnchantItem6);
+ addButton(Common::Rect(8, 74, 263, 82), Common::KEYCODE_7, nullptr, kItemsEnchantItem7);
+ addButton(Common::Rect(8, 83, 263, 91), Common::KEYCODE_8, nullptr, kItemsEnchantItem8);
+ addButton(Common::Rect(8, 92, 263, 100), Common::KEYCODE_9, nullptr, kItemsEnchantItem9);
} else {
bool flag = mode == ITEMMODE_BUY || mode == ITEMMODE_SELL || mode == ITEMMODE_IDENTIFY
|| mode == ITEMMODE_REPAIR;
- addButton(Common::Rect(12, 109, 36, 129), Res.KeyConstants.DialogsItems.KEY_WEAPONS, &_iconSprites);
- addButton(Common::Rect(46, 109, 70, 129), Res.KeyConstants.DialogsItems.KEY_ARMOR, &_iconSprites);
- addButton(Common::Rect(80, 109, 104, 129), Res.KeyConstants.DialogsItems.KEY_ACCESSORY, &_iconSprites);
- addButton(Common::Rect(114, 109, 138, 129), Res.KeyConstants.DialogsItems.KEY_MISC, &_iconSprites);
- addButton(Common::Rect(148, 109, 172, 129), flag ? Res.KeyConstants.DialogsItems.KEY_BUY : Res.KeyConstants.DialogsItems.KEY_EQUIP, &_iconSprites);
- addButton(Common::Rect(182, 109, 206, 129), flag ? Res.KeyConstants.DialogsItems.KEY_SELL : Res.KeyConstants.DialogsItems.KEY_REM, &_iconSprites);
- addButton(Common::Rect(216, 109, 240, 129), flag ? Res.KeyConstants.DialogsItems.KEY_IDENTIFY : Res.KeyConstants.DialogsItems.KEY_DISC, &_iconSprites);
- addButton(Common::Rect(250, 109, 274, 129), flag ? Res.KeyConstants.DialogsItems.KEY_FIX : Res.KeyConstants.DialogsItems.KEY_QUEST, &_iconSprites);
- addButton(Common::Rect(284, 109, 308, 129), Common::KEYCODE_ESCAPE, &_iconSprites);
- addButton(Common::Rect(8, 20, 263, 28), Common::KEYCODE_1);
- addButton(Common::Rect(8, 29, 263, 37), Common::KEYCODE_2);
- addButton(Common::Rect(8, 38, 263, 46), Common::KEYCODE_3);
- addButton(Common::Rect(8, 47, 263, 55), Common::KEYCODE_4);
- addButton(Common::Rect(8, 56, 263, 64), Common::KEYCODE_5);
- addButton(Common::Rect(8, 65, 263, 73), Common::KEYCODE_6);
- addButton(Common::Rect(8, 74, 263, 82), Common::KEYCODE_7);
- addButton(Common::Rect(8, 83, 263, 91), Common::KEYCODE_8);
- addButton(Common::Rect(8, 92, 263, 100), Common::KEYCODE_9);
+ addButton(Common::Rect(12, 109, 36, 129), Res.KeyConstants.DialogsItems.KEY_WEAPONS, &_iconSprites, kItemsWeapons);
+ addButton(Common::Rect(46, 109, 70, 129), Res.KeyConstants.DialogsItems.KEY_ARMOR, &_iconSprites, kItemsArmor);
+ addButton(Common::Rect(80, 109, 104, 129), Res.KeyConstants.DialogsItems.KEY_ACCESSORY, &_iconSprites, kItemsAccessories);
+ addButton(Common::Rect(114, 109, 138, 129), Res.KeyConstants.DialogsItems.KEY_MISC, &_iconSprites, kItemsMisc);
+ addButton(Common::Rect(148, 109, 172, 129), flag ? Res.KeyConstants.DialogsItems.KEY_BUY : Res.KeyConstants.DialogsItems.KEY_EQUIP, &_iconSprites, kItemsBuyOrEquip);
+ addButton(Common::Rect(182, 109, 206, 129), flag ? Res.KeyConstants.DialogsItems.KEY_SELL : Res.KeyConstants.DialogsItems.KEY_REM, &_iconSprites, kItemsSellOrRemove);
+ addButton(Common::Rect(216, 109, 240, 129), flag ? Res.KeyConstants.DialogsItems.KEY_IDENTIFY : Res.KeyConstants.DialogsItems.KEY_DISC, &_iconSprites, kItemsIdentifyOrDiscard);
+ addButton(Common::Rect(250, 109, 274, 129), flag ? Res.KeyConstants.DialogsItems.KEY_FIX : Res.KeyConstants.DialogsItems.KEY_QUEST, &_iconSprites, kItemsFixOrQuest);
+ addButton(Common::Rect(284, 109, 308, 129), Common::KEYCODE_ESCAPE, &_iconSprites, kItemsExit);
+ addButton(Common::Rect(8, 20, 263, 28), Common::KEYCODE_1, nullptr, kItemsItem1);
+ addButton(Common::Rect(8, 29, 263, 37), Common::KEYCODE_2, nullptr, kItemsItem2);
+ addButton(Common::Rect(8, 38, 263, 46), Common::KEYCODE_3, nullptr, kItemsItem3);
+ addButton(Common::Rect(8, 47, 263, 55), Common::KEYCODE_4, nullptr, kItemsItem4);
+ addButton(Common::Rect(8, 56, 263, 64), Common::KEYCODE_5, nullptr, kItemsItem5);
+ addButton(Common::Rect(8, 65, 263, 73), Common::KEYCODE_6, nullptr, kItemsItem6);
+ addButton(Common::Rect(8, 74, 263, 82), Common::KEYCODE_7, nullptr, kItemsItem7);
+ addButton(Common::Rect(8, 83, 263, 91), Common::KEYCODE_8, nullptr, kItemsItem8);
+ addButton(Common::Rect(8, 92, 263, 100), Common::KEYCODE_9, nullptr, kItemsItem9);
addPartyButtons(_vm);
}
@@ -743,8 +845,23 @@ int ItemsDialog::doItemOptions(Character &c, int actionIndex, int itemIndex, Ite
// Inventory is empty
return category == CATEGORY_MISC ? 0 : 2;
- if (itemIndex < 0 || itemIndex > 8)
- itemIndex = ItemSelectionDialog::show(actionIndex, items);
+ if (itemIndex < 0 || itemIndex > 8) {
+ // Populate item button texts. Text for items starts at keycode 1 and ends at keycode 9
+ Common::StringArray ttsItemButtonTexts;
+#ifdef USE_TTS
+ for (uint i = 0; i < _buttons.size(); ++i) {
+ if (_buttons[i]._value == Common::KeyCode::KEYCODE_1) {
+ if (_buttons[i]._ttsIndex >= _buttonTexts.size()) {
+ break;
+ }
+
+ ttsItemButtonTexts.assign(_buttonTexts.begin() + _buttons[i]._ttsIndex, _buttonTexts.end());
+ break;
+ }
+ }
+#endif
+ itemIndex = ItemSelectionDialog::show(actionIndex, items, ttsItemButtonTexts);
+ }
if (itemIndex != -1) {
XeenItem &item = items[itemIndex];
@@ -979,10 +1096,41 @@ void ItemsDialog::itemToGold(Character &c, int itemIndex, ItemCategory category,
}
}
+#ifdef USE_TTS
+
+void ItemsDialog::speakText(const Common::String &text, uint8 headerCount, uint8 lineCount) {
+ uint index = 0;
+ _vm->sayText(getNextTextSection(text, index, headerCount));
+
+ uint startingIndex = 0;
+
+ for (uint i = 0; i < _buttonTexts.size(); ++i) {
+ if (_buttonTexts[i].empty()) {
+ startingIndex = i;
+ break;
+ }
+ }
+
+ // In some cases, each item has 3 fields: number, name, and cost. In others, it only has the number and name
+ // This generally corresponds to the number of fields in the header (i.e. "Weapons for Character" and "Cost" is 2 headers
+ // and 2 fields, versus just "Weapons for Character" that's 1 header with 1 field)
+ uint fieldsPerSection = (headerCount >= 2 || g_vm->_extOptions._showItemCosts) ? 3 : 2;
+ for (uint i = 0; i < lineCount; ++i) {
+ Common::String itemInfo = getNextTextSection(text, index, fieldsPerSection, ", ");
+ _vm->sayText(itemInfo);
+
+ if (startingIndex != 0 && i + startingIndex < _buttonTexts.size()) {
+ _buttonTexts[i + startingIndex] = itemInfo;
+ }
+ }
+}
+
+#endif
+
/*------------------------------------------------------------------------*/
-int ItemSelectionDialog::show(int actionIndex, InventoryItems &items) {
- ItemSelectionDialog *dlg = new ItemSelectionDialog(g_vm, actionIndex, items);
+int ItemSelectionDialog::show(int actionIndex, InventoryItems &items, const Common::StringArray &ttsItemButtonTexts) {
+ ItemSelectionDialog *dlg = new ItemSelectionDialog(g_vm, actionIndex, items, ttsItemButtonTexts);
int result = dlg->execute();
delete dlg;
@@ -992,15 +1140,15 @@ int ItemSelectionDialog::show(int actionIndex, InventoryItems &items) {
void ItemSelectionDialog::loadButtons() {
_icons.load("esc.icn");
addButton(Common::Rect(235, 111, 259, 131), Common::KEYCODE_ESCAPE, &_icons);
- addButton(Common::Rect(8, 20, 263, 28), Common::KEYCODE_1);
- addButton(Common::Rect(8, 29, 263, 37), Common::KEYCODE_2);
- addButton(Common::Rect(8, 38, 263, 46), Common::KEYCODE_3);
- addButton(Common::Rect(8, 47, 263, 55), Common::KEYCODE_4);
- addButton(Common::Rect(8, 56, 263, 64), Common::KEYCODE_5);
- addButton(Common::Rect(8, 65, 263, 73), Common::KEYCODE_6);
- addButton(Common::Rect(8, 74, 263, 82), Common::KEYCODE_7);
- addButton(Common::Rect(8, 83, 263, 91), Common::KEYCODE_8);
- addButton(Common::Rect(8, 92, 263, 100), Common::KEYCODE_9);
+ addButton(Common::Rect(8, 20, 263, 28), Common::KEYCODE_1, nullptr, kItemSelectionItem1);
+ addButton(Common::Rect(8, 29, 263, 37), Common::KEYCODE_2, nullptr, kItemSelectionItem2);
+ addButton(Common::Rect(8, 38, 263, 46), Common::KEYCODE_3, nullptr, kItemSelectionItem3);
+ addButton(Common::Rect(8, 47, 263, 55), Common::KEYCODE_4, nullptr, kItemSelectionItem4);
+ addButton(Common::Rect(8, 56, 263, 64), Common::KEYCODE_5, nullptr, kItemSelectionItem5);
+ addButton(Common::Rect(8, 65, 263, 73), Common::KEYCODE_6, nullptr, kItemSelectionItem6);
+ addButton(Common::Rect(8, 74, 263, 82), Common::KEYCODE_7, nullptr, kItemSelectionItem7);
+ addButton(Common::Rect(8, 83, 263, 91), Common::KEYCODE_8, nullptr, kItemSelectionItem8);
+ addButton(Common::Rect(8, 92, 263, 100), Common::KEYCODE_9, nullptr, kItemSelectionItem9);
}
int ItemSelectionDialog::execute() {
@@ -1013,6 +1161,12 @@ int ItemSelectionDialog::execute() {
_icons.draw(0, 0, Common::Point(235, 111));
w.update();
+#ifdef USE_TTS
+ for (uint i = 0; i < _buttonTexts.size(); ++i) {
+ _vm->sayText(_buttonTexts[i]);
+ }
+#endif
+
int itemIndex = -1;
while (!_vm->shouldExit()) {
_buttonValue = 0;
diff --git a/engines/mm/xeen/dialogs/dialogs_items.h b/engines/mm/xeen/dialogs/dialogs_items.h
index 7c3f0361b63..12c01eac24f 100644
--- a/engines/mm/xeen/dialogs/dialogs_items.h
+++ b/engines/mm/xeen/dialogs/dialogs_items.h
@@ -73,6 +73,10 @@ private:
ItemCategory category, ItemsMode mode);
void itemToGold(Character &c, int itemIndex, ItemCategory category, ItemsMode mode);
+
+#ifdef USE_TTS
+ void speakText(const Common::String &text, uint8 headerCount, uint8 lineCount);
+#endif
public:
static Character *show(XeenEngine *vm, Character *c, ItemsMode mode);
};
@@ -83,9 +87,12 @@ private:
int _actionIndex;
InventoryItems &_items;
- ItemSelectionDialog(XeenEngine *vm, int actionIndex, InventoryItems &items) : ButtonContainer(vm),
+ ItemSelectionDialog(XeenEngine *vm, int actionIndex, InventoryItems &items, const Common::StringArray &ttsItemButtonTexts) : ButtonContainer(vm),
_actionIndex(actionIndex), _items(items) {
loadButtons();
+#ifdef USE_TTS
+ _buttonTexts = ttsItemButtonTexts;
+#endif
}
/**
@@ -101,11 +108,12 @@ private:
public:
/**
* Shows the dialog
- * @param actionIndex Current action type
- * @param items Currently active items category
- * @returns Selected item index
+ * @param actionIndex Current action type
+ * @param items Currently active items category
+ * @param ttsItemButtonTexts Text for each item button
+ * @returns Selected item index
*/
- static int show(int actionIndex, InventoryItems &items);
+ static int show(int actionIndex, InventoryItems &items, const Common::StringArray &ttsItemButtonTexts);
};
diff --git a/engines/mm/xeen/dialogs/dialogs_map.cpp b/engines/mm/xeen/dialogs/dialogs_map.cpp
index 7c65c239d09..4ecb788a859 100644
--- a/engines/mm/xeen/dialogs/dialogs_map.cpp
+++ b/engines/mm/xeen/dialogs/dialogs_map.cpp
@@ -78,6 +78,11 @@ void MapDialog::execute() {
windows[5].open();
bool drawFlag = true;
+#ifdef USE_TTS
+ _vm->stopTextToSpeech();
+#endif
+ bool ttsVoiceText = true;
+
events.updateGameCounter();
do {
if (drawFlag)
@@ -105,7 +110,8 @@ void MapDialog::execute() {
windows[5].writeString(Common::String::format(Res.MAP_TEXT,
map._mazeName.c_str(), party._mazePosition.x,
- party._mazePosition.y, Res.DIRECTION_TEXT[party._mazeDirection]));
+ party._mazePosition.y, Res.DIRECTION_TEXT[party._mazeDirection]), ttsVoiceText);
+ ttsVoiceText = false;
windows[5].update();
windows[3].update();
@@ -113,6 +119,9 @@ void MapDialog::execute() {
drawFlag = false;
} while (!_vm->shouldExit() && !events.isKeyMousePressed());
+#ifdef USE_TTS
+ _vm->stopTextToSpeech();
+#endif
events.clearEvents();
windows[5].close();
}
diff --git a/engines/mm/xeen/dialogs/dialogs_message.cpp b/engines/mm/xeen/dialogs/dialogs_message.cpp
index 1b17142bdd4..7f8d65d0c37 100644
--- a/engines/mm/xeen/dialogs/dialogs_message.cpp
+++ b/engines/mm/xeen/dialogs/dialogs_message.cpp
@@ -38,6 +38,9 @@ void MessageDialog::execute(const Common::String &msg, MessageWaitType waitType)
Windows &windows = *_vm->_windows;
Window &w = windows[6];
+#ifdef USE_TTS
+ _vm->stopTextToSpeech();
+#endif
w.open();
w.writeString(msg);
w.update();
@@ -48,6 +51,9 @@ void MessageDialog::execute(const Common::String &msg, MessageWaitType waitType)
events.pollEventsAndWait();
events.clearEvents();
+#ifdef USE_TTS
+ _vm->stopTextToSpeech();
+#endif
break;
case WT_ANIMATED_WAIT:
@@ -66,6 +72,9 @@ void MessageDialog::execute(const Common::String &msg, MessageWaitType waitType)
if (checkEvents(_vm))
break;
} while (!_vm->shouldExit() && !_buttonValue);
+#ifdef USE_TTS
+ _vm->stopTextToSpeech();
+#endif
break;
case WT_LOC_WAIT:
@@ -117,6 +126,9 @@ void CantCast::execute(int spellId, int componentNum) {
events.clearEvents();
w.close();
+#ifdef USE_TTS
+ _vm->stopTextToSpeech();
+#endif
_vm->_mode = oldMode;
}
diff --git a/engines/mm/xeen/dialogs/dialogs_party.cpp b/engines/mm/xeen/dialogs/dialogs_party.cpp
index f1717cf75fd..7ec1d817c55 100644
--- a/engines/mm/xeen/dialogs/dialogs_party.cpp
+++ b/engines/mm/xeen/dialogs/dialogs_party.cpp
@@ -32,8 +32,32 @@
namespace MM {
namespace Xeen {
+#ifdef USE_TTS
+
+static const uint8 kPartyDialogFaceCount = 4;
+static const uint8 kPartyDialogButtonCount = 6;
+static const uint8 kPartyDialogInfoPerFaceCount = 4;
+
+#endif
+
+enum PartyDialogButtonTTSTextIndex {
+ kPartyDialogUp = 0,
+ kPartyDialogDown = 1,
+ kPartyDialogDelete = 2,
+ kPartyDialogRemove = 3,
+ kPartyDialogCreate = 4,
+ kPartyDialogExit = 5,
+ kPartyDialogFace1 = 6,
+ kPartyDialogFace2 = 7,
+ kPartyDialogFace3 = 8,
+ kPartyDialogFace4 = 9
+};
+
PartyDialog::PartyDialog(XeenEngine *vm) : ButtonContainer(vm),
PartyDrawer(vm), _vm(vm) {
+#ifdef USE_TTS
+ _faceCount = 0;
+#endif
initDrawStructs();
}
@@ -58,6 +82,11 @@ void PartyDialog::execute() {
loadButtons();
setupBackground();
+ Common::String ttsMessage;
+#ifdef USE_TTS
+ bool returnFromCreate = false;
+#endif
+
while (!_vm->shouldExit()) {
_vm->_mode = MODE_INTERACTIVE;
@@ -68,7 +97,13 @@ void PartyDialog::execute() {
Window &w = windows[11];
w.open();
setupFaces(startingChar, false);
- w.writeString(Common::String::format(Res.PARTY_DIALOG_TEXT, _partyDetails.c_str()));
+ w.writeString(Common::String::format(Res.PARTY_DIALOG_TEXT, _partyDetails.c_str()), false, &ttsMessage);
+
+#ifdef USE_TTS
+ speakText(ttsMessage, returnFromCreate);
+ returnFromCreate = false;
+#endif
+
w.drawList(&_faceDrawStructs[0], 4);
_uiSprites.draw(w, 0, Common::Point(16, 100));
@@ -206,6 +241,9 @@ void PartyDialog::execute() {
screen.fadeOut();
screen.blitFrom(savedBg);
windows[0].update();
+#ifdef USE_TTS
+ returnFromCreate = true;
+#endif
modeFlag = true;
breakFlag = true;
@@ -274,13 +312,13 @@ void PartyDialog::loadCharacters() {
void PartyDialog::loadButtons() {
_uiSprites.load("inn.icn");
- addButton(Common::Rect(16, 100, 40, 120), Common::KEYCODE_UP, &_uiSprites);
- addButton(Common::Rect(52, 100, 76, 120), Common::KEYCODE_DOWN, &_uiSprites);
+ addButton(Common::Rect(16, 100, 40, 120), Common::KEYCODE_UP, &_uiSprites, kPartyDialogUp);
+ addButton(Common::Rect(52, 100, 76, 120), Common::KEYCODE_DOWN, &_uiSprites, kPartyDialogDown);
- addButton(Common::Rect(87, 100, 111, 120), Res.KeyConstants.DialogsParty.KEY_DELETE, &_uiSprites);
- addButton(Common::Rect(122, 100, 146, 120), Res.KeyConstants.DialogsParty.KEY_REMOVE, &_uiSprites);
- addButton(Common::Rect(157, 100, 181, 120), Res.KeyConstants.DialogsParty.KEY_CREATE, &_uiSprites);
- addButton(Common::Rect(192, 100, 216, 120), Res.KeyConstants.DialogsParty.KEY_EXIT, &_uiSprites);
+ addButton(Common::Rect(87, 100, 111, 120), Res.KeyConstants.DialogsParty.KEY_DELETE, &_uiSprites, kPartyDialogDelete);
+ addButton(Common::Rect(122, 100, 146, 120), Res.KeyConstants.DialogsParty.KEY_REMOVE, &_uiSprites, kPartyDialogRemove);
+ addButton(Common::Rect(157, 100, 181, 120), Res.KeyConstants.DialogsParty.KEY_CREATE, &_uiSprites, kPartyDialogCreate);
+ addButton(Common::Rect(192, 100, 216, 120), Res.KeyConstants.DialogsParty.KEY_EXIT, &_uiSprites, kPartyDialogExit);
addButton(Common::Rect(0, 0, 0, 0), Common::KEYCODE_ESCAPE);
}
@@ -309,11 +347,14 @@ void PartyDialog::setupFaces(int firstDisplayChar, bool updateFlag) {
// Reset the button areas for the display character images
while (_buttons.size() > 7)
_buttons.remove_at(7);
- addButton(Common::Rect(16, 16, 48, 48), Common::KEYCODE_1);
- addButton(Common::Rect(117, 16, 149, 48), Common::KEYCODE_2);
- addButton(Common::Rect(59, 59, 91, 91), Common::KEYCODE_3);
- addButton(Common::Rect(117, 59, 151, 91), Common::KEYCODE_4);
+ addButton(Common::Rect(16, 16, 48, 48), Common::KEYCODE_1, nullptr, kPartyDialogFace1);
+ addButton(Common::Rect(117, 16, 149, 48), Common::KEYCODE_2, nullptr, kPartyDialogFace2);
+ addButton(Common::Rect(59, 59, 91, 91), Common::KEYCODE_3, nullptr, kPartyDialogFace3);
+ addButton(Common::Rect(117, 59, 151, 91), Common::KEYCODE_4, nullptr, kPartyDialogFace4);
+#ifdef USE_TTS
+ _faceCount = 0;
+#endif
for (posIndex = 0; posIndex < 4; ++posIndex) {
charId = (firstDisplayChar + posIndex) >= (int)_charList.size() ? -1 :
@@ -326,6 +367,10 @@ void PartyDialog::setupFaces(int firstDisplayChar, bool updateFlag) {
break;
}
+#ifdef USE_TTS
+ _faceCount++;
+#endif
+
Common::Rect &b = _buttons[7 + posIndex]._bounds;
b.moveTo((posIndex & 1) ? 117 : 16, b.top);
Character &ps = party._roster[_charList[firstDisplayChar + posIndex]];
@@ -359,7 +404,11 @@ void PartyDialog::startingCharChanged(int firstDisplayChar) {
Window &w = windows[11];
setupFaces(firstDisplayChar, true);
- w.writeString(Common::String::format(Res.PARTY_DIALOG_TEXT, _partyDetails.c_str()));
+ Common::String ttsMessage;
+ w.writeString(Common::String::format(Res.PARTY_DIALOG_TEXT, _partyDetails.c_str()), false, &ttsMessage);
+#ifdef USE_TTS
+ speakText(ttsMessage, false);
+#endif
w.drawList(_faceDrawStructs, 4);
_uiSprites.draw(w, 0, Common::Point(16, 100));
@@ -391,10 +440,10 @@ int PartyDialog::selectCharacter(bool isDelete, int firstDisplayChar) {
saveButtons();
addButton(Common::Rect(225, isDelete ? 120 : 84, 249, isDelete ? 140 : 104),
Common::KEYCODE_ESCAPE, &iconSprites);
- addButton(Common::Rect(16, 16, 48, 48), Common::KEYCODE_1);
- addButton(Common::Rect(117, 16, 149, 48), Common::KEYCODE_2);
- addButton(Common::Rect(16, 59, 48, 91), Common::KEYCODE_3);
- addButton(Common::Rect(117, 59, 149, 91), Common::KEYCODE_4);
+ addButton(Common::Rect(16, 16, 48, 48), Common::KEYCODE_1, nullptr, kPartyDialogFace1);
+ addButton(Common::Rect(117, 16, 149, 48), Common::KEYCODE_2, nullptr, kPartyDialogFace2);
+ addButton(Common::Rect(16, 59, 48, 91), Common::KEYCODE_3, nullptr, kPartyDialogFace3);
+ addButton(Common::Rect(117, 59, 149, 91), Common::KEYCODE_4, nullptr, kPartyDialogFace4);
addPartyButtons(_vm);
int result = -1, v;
@@ -444,5 +493,39 @@ int PartyDialog::selectCharacter(bool isDelete, int firstDisplayChar) {
return result == -1 ? 0 : result;
}
+#ifdef USE_TTS
+
+void PartyDialog::speakText(const Common::String &text, bool isCharacterCreation) {
+ uint index = 0;
+
+ // After returning from the character creation screen, the text is duplicated, with the first iteration being
+ // outdated
+ if (isCharacterCreation) {
+ for (uint i = 0; i < _faceCount; ++i) {
+ getNextTextSection(text, index, kPartyDialogInfoPerFaceCount);
+ }
+
+ getNextTextSection(text, index, kPartyDialogButtonCount);
+ }
+
+ _buttonTexts.clear();
+
+ Common::String faceTexts[kPartyDialogFaceCount];
+ for (uint8 i = 0; i < _faceCount; ++i) {
+ faceTexts[i] = getNextTextSection(text, index, kPartyDialogInfoPerFaceCount);
+ _vm->sayText(faceTexts[i]);
+ }
+
+ Common::String buttonTexts = addNextTextToButtons(text, index, kPartyDialogButtonCount);
+ _vm->sayText(buttonTexts);
+
+ // Since the number of faces can vary, sort them after the bottom buttons
+ for (uint8 i = 0; i < _faceCount; ++i) {
+ _buttonTexts.push_back(faceTexts[i]);
+ }
+}
+
+#endif
+
} // End of namespace Xeen
} // End of namespace MM
diff --git a/engines/mm/xeen/dialogs/dialogs_party.h b/engines/mm/xeen/dialogs/dialogs_party.h
index 5eed32c03ac..d89f1fd6bc2 100644
--- a/engines/mm/xeen/dialogs/dialogs_party.h
+++ b/engines/mm/xeen/dialogs/dialogs_party.h
@@ -41,6 +41,9 @@ private:
DrawStruct _faceDrawStructs[4];
Common::String _partyDetails;
Common::Array<int> _charList;
+#ifdef USE_TTS
+ uint8 _faceCount;
+#endif
/**
* Constructor
@@ -80,6 +83,10 @@ private:
void startingCharChanged(int firstDisplayChar);
int selectCharacter(bool isDelete, int firstDisplayChar);
+
+#ifdef USE_TTS
+ void speakText(const Common::String &text, bool isCharacterCreation);
+#endif
public:
/**
* Show the Party dialog
diff --git a/engines/mm/xeen/dialogs/dialogs_query.cpp b/engines/mm/xeen/dialogs/dialogs_query.cpp
index 5b5a8cd34fe..ca2fa77591b 100644
--- a/engines/mm/xeen/dialogs/dialogs_query.cpp
+++ b/engines/mm/xeen/dialogs/dialogs_query.cpp
@@ -61,6 +61,9 @@ bool Confirm::execute(const Common::String &msg, int mode) {
}
}
+#ifdef USE_TTS
+ _vm->stopTextToSpeech();
+#endif
w.writeString(msg);
w.update();
@@ -84,6 +87,9 @@ bool Confirm::execute(const Common::String &msg, int mode) {
}
}
+#ifdef USE_TTS
+ _vm->stopTextToSpeech();
+#endif
events.clearEvents();
w.close();
return result;
diff --git a/engines/mm/xeen/dialogs/dialogs_quests.cpp b/engines/mm/xeen/dialogs/dialogs_quests.cpp
index f127c7dae96..0c33317124f 100644
--- a/engines/mm/xeen/dialogs/dialogs_quests.cpp
+++ b/engines/mm/xeen/dialogs/dialogs_quests.cpp
@@ -28,6 +28,15 @@
namespace MM {
namespace Xeen {
+enum QuestButtonTTSTextIndex {
+ kQuestItems = 0,
+ kQuestCurrentQuests = 1,
+ kQuestAutoNotes = 2,
+ kQuestUp = 3,
+ kQuestDown = 4,
+ kQuestExit = 5
+};
+
#define MAX_DIALOG_LINES 128
void Quests::show(XeenEngine *vm) {
@@ -61,8 +70,14 @@ void Quests::execute() {
windowFlag = true;
}
- windows[29].writeString(Res.QUESTS_DIALOG_TEXT);
+ Common::String ttsButtons;
+ windows[29].writeString(Res.QUESTS_DIALOG_TEXT, false, &ttsButtons);
+#ifdef USE_TTS
+ _vm->stopTextToSpeech();
+ setButtonTexts(ttsButtons);
+#endif
drawButtons(&windows[0]);
+ Common::String ttsMessage;
while (!_vm->shouldExit()) {
Common::String lines[MAX_DIALOG_LINES];
@@ -116,6 +131,7 @@ void Quests::execute() {
}
}
+ ttsMessage.clear();
if (count == 0) {
windows[30].writeString(Res.NO_QUEST_ITEMS);
} else {
@@ -125,7 +141,11 @@ void Quests::execute() {
lines[topRow + 4].c_str(), lines[topRow + 5].c_str(),
lines[topRow + 6].c_str(), lines[topRow + 7].c_str(),
lines[topRow + 8].c_str()
- ));
+ ), false, &ttsMessage);
+ ttsMessage.replace('*', ' ');
+#ifdef USE_TTS
+ _vm->sayText(ttsMessage);
+#endif
}
break;
@@ -152,8 +172,13 @@ void Quests::execute() {
if (count == 0)
lines[1] = Res.NO_CURRENT_QUESTS;
+ ttsMessage.clear();
windows[30].writeString(Common::String::format(Res.CURRENT_QUESTS_DATA,
- lines[topRow].c_str(), lines[topRow + 1].c_str(), lines[topRow + 2].c_str()));
+ lines[topRow].c_str(), lines[topRow + 1].c_str(), lines[topRow + 2].c_str()), false, &ttsMessage);
+ ttsMessage.replace('*', ' ');
+#ifdef USE_TTS
+ _vm->sayText(ttsMessage);
+#endif
break;
case AUTO_NOTES: {
@@ -211,6 +236,9 @@ void Quests::execute() {
windows[30].writeString("\v000\t000");
windows[24].update();
+#ifdef USE_TTS
+ g_vm->sayText(ttsButtons);
+#endif
// Key handling
_buttonValue = 0;
@@ -260,13 +288,13 @@ void Quests::execute() {
void Quests::addButtons() {
_iconSprites.load("quest.icn");
- addButton(Common::Rect(12, 109, 36, 129), Res.KeyConstants.DialogsQuests.KEY_QUEST_ITEMS, &_iconSprites);
- addButton(Common::Rect(80, 109, 104, 129), Res.KeyConstants.DialogsQuests.KEY_CURRENT_QUESTS, &_iconSprites);
- addButton(Common::Rect(148, 109, 172, 129), Res.KeyConstants.DialogsQuests.KEY_AUTO_NOTES, &_iconSprites);
+ addButton(Common::Rect(12, 109, 36, 129), Res.KeyConstants.DialogsQuests.KEY_QUEST_ITEMS, &_iconSprites, kQuestItems);
+ addButton(Common::Rect(80, 109, 104, 129), Res.KeyConstants.DialogsQuests.KEY_CURRENT_QUESTS, &_iconSprites, kQuestCurrentQuests);
+ addButton(Common::Rect(148, 109, 172, 129), Res.KeyConstants.DialogsQuests.KEY_AUTO_NOTES, &_iconSprites, kQuestAutoNotes);
- addButton(Common::Rect(216, 109, 240, 129), Common::KEYCODE_UP, &_iconSprites);
- addButton(Common::Rect(250, 109, 274, 129), Common::KEYCODE_DOWN, &_iconSprites);
- addButton(Common::Rect(284, 109, 308, 129), Common::KEYCODE_ESCAPE, &_iconSprites);
+ addButton(Common::Rect(216, 109, 240, 129), Common::KEYCODE_UP, &_iconSprites, kQuestUp);
+ addButton(Common::Rect(250, 109, 274, 129), Common::KEYCODE_DOWN, &_iconSprites, kQuestDown);
+ addButton(Common::Rect(284, 109, 308, 129), Common::KEYCODE_ESCAPE, &_iconSprites, kQuestExit);
}
void Quests::loadQuestNotes() {
diff --git a/engines/mm/xeen/dialogs/dialogs_quick_fight.cpp b/engines/mm/xeen/dialogs/dialogs_quick_fight.cpp
index 12c299ed7d4..45fc813a35c 100644
--- a/engines/mm/xeen/dialogs/dialogs_quick_fight.cpp
+++ b/engines/mm/xeen/dialogs/dialogs_quick_fight.cpp
@@ -26,6 +26,18 @@
namespace MM {
namespace Xeen {
+#ifdef USE_TTS
+
+static const uint8 kQuickFightInfoCount = 4;
+static const uint8 kQuickFightButtonCount = 2;
+
+#endif
+
+enum QuickFightButtonTTSTextIndex {
+ kQuickFightNext = 0,
+ kQuickFightExit = 1
+};
+
void QuickFight::show(XeenEngine *vm, Character *currentChar) {
QuickFight *dlg = new QuickFight(vm, currentChar);
dlg->execute();
@@ -45,15 +57,24 @@ void QuickFight::execute() {
Windows &windows = *_vm->_windows;
Window &w = windows[10];
w.open();
+ bool ttsVoiceText = true;
do {
// Draw the dialog text and buttons
Common::String msg = Common::String::format(Res.QUICK_FIGHT_TEXT,
_currentChar->_name.c_str(),
Res.QUICK_FIGHT_OPTIONS[_currentChar->_quickOption]);
- w.writeString(msg);
+ Common::String ttsMessage;
+ w.writeString(msg, ttsVoiceText, &ttsMessage);
drawButtons(&w);
+#ifdef USE_TTS
+ if (ttsVoiceText) {
+ setUpButtons(ttsMessage);
+ ttsVoiceText = false;
+ }
+#endif
+
// Wait for selection
_buttonValue = 0;
events.updateGameCounter();
@@ -76,11 +97,17 @@ void QuickFight::execute() {
if (charIdx < (int)combat._combatParty.size()) {
// Highlight new character
_currentChar = &party._activeParty[charIdx];
+#ifdef USE_TTS
+ _vm->sayText(_currentChar->_name + ": " + Res.QUICK_FIGHT_OPTIONS[_currentChar->_quickOption], Common::TextToSpeechManager::INTERRUPT);
+#endif
intf.highlightChar(charIdx);
}
} else if (Common::KEYCODE_n == _buttonValue ||
Res.KeyConstants.DialogsQuickFight.KEY_NEXT == _buttonValue) {
_currentChar->_quickOption = (QuickAction)(((int)_currentChar->_quickOption + 1) % 4);
+#ifdef USE_TTS
+ _vm->sayText(Res.QUICK_FIGHT_OPTIONS[_currentChar->_quickOption], Common::TextToSpeechManager::INTERRUPT);
+#endif
}
} while (_buttonValue != Common::KEYCODE_RETURN && _buttonValue != Common::KEYCODE_ESCAPE);
@@ -90,10 +117,22 @@ void QuickFight::execute() {
void QuickFight::loadButtons() {
_icons.load("train.icn");
- addButton(Common::Rect(281, 108, 305, 128), Common::KEYCODE_ESCAPE, &_icons);
+ addButton(Common::Rect(281, 108, 305, 128), Common::KEYCODE_ESCAPE, &_icons, kQuickFightExit);
- addButton(Common::Rect(242, 108, 266, 128), Res.KeyConstants.DialogsQuickFight.KEY_NEXT, &_icons);
+ addButton(Common::Rect(242, 108, 266, 128), Res.KeyConstants.DialogsQuickFight.KEY_NEXT, &_icons, kQuickFightNext);
}
+#ifdef USE_TTS
+
+void QuickFight::setUpButtons(const Common::String &text) {
+ if (_buttonTexts.empty()) {
+ uint index = 0;
+ getNextTextSection(text, index, kQuickFightInfoCount);
+ addNextTextToButtons(text, index, kQuickFightButtonCount);
+ }
+}
+
+#endif
+
} // End of namespace Xeen
} // End of namespace MM
diff --git a/engines/mm/xeen/dialogs/dialogs_quick_fight.h b/engines/mm/xeen/dialogs/dialogs_quick_fight.h
index 6cc2ae855ae..0efacdb947c 100644
--- a/engines/mm/xeen/dialogs/dialogs_quick_fight.h
+++ b/engines/mm/xeen/dialogs/dialogs_quick_fight.h
@@ -48,6 +48,14 @@ private:
* Load butons for the dialog
*/
void loadButtons();
+
+#ifdef USE_TTS
+ /**
+ * Sets up button texts
+ * @param text Text for buttons, with each button text separated by newlines
+ */
+ void setUpButtons(const Common::String &text);
+#endif
public:
/**
* Show the dialog
diff --git a/engines/mm/xeen/dialogs/dialogs_quick_ref.cpp b/engines/mm/xeen/dialogs/dialogs_quick_ref.cpp
index 70ce7e2dc41..f00e96af271 100644
--- a/engines/mm/xeen/dialogs/dialogs_quick_ref.cpp
+++ b/engines/mm/xeen/dialogs/dialogs_quick_ref.cpp
@@ -27,6 +27,13 @@
namespace MM {
namespace Xeen {
+#ifdef USE_TTS
+
+static const uint8 kQuickRefUpperHeaderCount = 8;
+static const uint8 kQuickRefLowerHeaderCount = 3;
+
+#endif
+
void QuickReferenceDialog::show(XeenEngine *vm) {
QuickReferenceDialog *dlg = new QuickReferenceDialog(vm);
dlg->execute();
@@ -59,8 +66,9 @@ void QuickReferenceDialog::execute() {
Common::String lines[8];
events.setCursor(0);
- for (uint idx = 0; idx < (combat._globalCombat == 2 ? combat._combatParty.size() :
- party._activeParty.size()); ++idx) {
+
+ uint partySize = combat._globalCombat == 2 ? combat._combatParty.size() : party._activeParty.size();
+ for (uint idx = 0; idx < partySize; ++idx) {
Character &c = combat._globalCombat == 2 ? *combat._combatParty[idx] :
party._activeParty[idx];
const char **tmpConditions = c._sex == FEMALE ? (const char **)Res.CONDITION_NAMES_F : (const char **)Res.CONDITION_NAMES_M;
@@ -92,20 +100,66 @@ void QuickReferenceDialog::execute() {
if (!windowOpen)
w.open();
+ Common::String ttsMessage;
+
// Turn off reduced font mode and then print everything
w.writeString("\1");
- w.writeString(msg);
+ w.writeString(msg, false, &ttsMessage);
w.update();
+#ifdef USE_TTS
+ speakText(ttsMessage, partySize);
+#endif
+
// Wait for a key/mouse press
events.clearEvents();
while (!_vm->shouldExit() && !events.isKeyMousePressed())
events.pollEventsAndWait();
+#ifdef USE_TTS
+ _vm->stopTextToSpeech();
+#endif
events.clearEvents();
if (!windowOpen)
w.close();
}
+#ifdef USE_TTS
+
+void QuickReferenceDialog::speakText(const Common::String &text, uint partySize) const {
+ uint index = 0;
+
+ // "Quick reference chart"
+ _vm->sayText(getNextTextSection(text, index), Common::TextToSpeechManager::INTERRUPT);
+
+ // Split the header descriptors ("Name", "Cond", etc.) into an array
+ // If we voice everything as originally ordered, the context isn't clear (i.e. "Name, Class, Level, ..."
+ // will be voiced as one piece, which may make it difficult to match each field with the corresponding info of
+ // each party member)
+ Common::String headers[kQuickRefUpperHeaderCount];
+ for (uint8 i = 0; i < kQuickRefUpperHeaderCount; ++i) {
+ headers[i] = getNextTextSection(text, index);
+ }
+
+ // Voice each party member's description
+ for (uint i = 0; i < partySize; ++i) {
+ for (uint8 j = 0; j < kQuickRefUpperHeaderCount; ++j) {
+ _vm->sayText(headers[j] + ": " + getNextTextSection(text, index));
+ }
+ }
+
+ // Split the gold/gems/food descriptors into an array
+ for (uint8 i = 0; i < kQuickRefLowerHeaderCount; ++i) {
+ headers[i] = getNextTextSection(text, index);
+ }
+
+ // Voice the gold/gems/food amounts
+ for (uint8 i = 0; i < kQuickRefLowerHeaderCount; ++i) {
+ _vm->sayText(headers[i] + ": " + getNextTextSection(text, index));
+ }
+}
+
+#endif
+
} // End of namespace Xeen
} // End of namespace MM
diff --git a/engines/mm/xeen/dialogs/dialogs_quick_ref.h b/engines/mm/xeen/dialogs/dialogs_quick_ref.h
index cb3e3adbeb4..5115f97df93 100644
--- a/engines/mm/xeen/dialogs/dialogs_quick_ref.h
+++ b/engines/mm/xeen/dialogs/dialogs_quick_ref.h
@@ -37,6 +37,10 @@ private:
const char *getDaysPlurals(int val);
void execute();
+
+#ifdef USE_TTS
+ void speakText(const Common::String &text, uint partySize) const;
+#endif
public:
static void show(XeenEngine *vm);
};
diff --git a/engines/mm/xeen/dialogs/dialogs_spells.cpp b/engines/mm/xeen/dialogs/dialogs_spells.cpp
index d224b449805..f4bdf62d51e 100644
--- a/engines/mm/xeen/dialogs/dialogs_spells.cpp
+++ b/engines/mm/xeen/dialogs/dialogs_spells.cpp
@@ -30,6 +30,72 @@
namespace MM {
namespace Xeen {
+#ifdef USE_TTS
+
+static const uint8 kSpellsButtonCount = 3;
+static const uint8 kSpellsSpellCount = 10;
+
+static const uint8 kSelectElementButtonCount = 4;
+
+static const uint8 kLloydsBeaconInfoCount = 5;
+static const uint8 kLloydsBeaconButtonCount = 2;
+
+static const uint8 kIdentifyMonsterHeaderCount = 5;
+static const uint8 kIdentifyMonsterMaxMonsterCount = 3;
+
+static const char *selectButton[] = {
+ "Select", // English
+ "O.K.", // German
+ "S\202lectionner", // French (not translated in-game)
+ "Seleccionar", // Spanish (shown as "Selecc" in-game)
+ "\x82\xeb\xa1\xae\xe0", // Russian (ÐÑбоÑ)
+ "\xbf\xef\xa9\x77" // Chinese (é¸å®)
+};
+
+static const char *exitButton[] = {
+ "Exit", // English
+ "Ende", // German
+ "Sortir", // French (not translated in-game)
+ "ESC", // Spanish
+ "\x82\xeb\xe5\xae\xa4", // Russian (ÐÑÑ
од)
+ "\xc2\xf7" // Chinese (é¢)
+};
+
+#endif
+
+enum SpellsButtonTTSTextIndex {
+ kSpellsSpell1 = 0,
+ kSpellsSpell2 = 1,
+ kSpellsSpell3 = 2,
+ kSpellsSpell4 = 3,
+ kSpellsSpell5 = 4,
+ kSpellsSpell6 = 5,
+ kSpellsSpell7 = 6,
+ kSpellsSpell8 = 7,
+ kSpellsSpell9 = 8,
+ kSpellsSpell10 = 9,
+ kSpellsSelect = 10,
+ kSpellsExit = 11
+};
+
+enum CastSpellButtonTTSTextIndex {
+ kCastSpellCast = 0,
+ kCastSpellNew = 1,
+ kCastSpellExit = 2
+};
+
+enum SelectElementButtonTTSTextIndex {
+ kSelectElementFire = 0,
+ kSelectElementElectricity = 1,
+ kSelectElementCold = 2,
+ kSelectElementAcid = 3
+};
+
+enum LloydsBeaconButtonTTSTextIndex {
+ kLloydsBeaconSet = 0,
+ kLloydsBeaconReturn = 1
+};
+
Character *SpellsDialog::show(XeenEngine *vm, ButtonContainer *priorDialog,
Character *c, SpellDialogMode mode) {
SpellsDialog *dlg = new SpellsDialog(vm);
@@ -59,6 +125,11 @@ Character *SpellsDialog::execute(ButtonContainer *priorDialog, Character *c, int
int newSelection;
w.open();
+#ifdef USE_TTS
+ bool voiceText = true;
+ bool resetButtons = false;
+#endif
+
do {
if (!mode) {
if (!c->guildMember()) {
@@ -71,15 +142,16 @@ Character *SpellsDialog::execute(ButtonContainer *priorDialog, Character *c, int
Common::String title = Common::String::format(Res.BUY_SPELLS, c->_name.c_str());
Common::String msg = Common::String::format(Res.GUILD_OPTIONS,
title.c_str(), XeenEngine::printMil(party._gold).c_str());
- windows[10].writeString(msg);
+ windows[10].writeString(msg, false);
priorDialog->drawButtons(&windows[10]);
}
_spells.clear();
const char *errorMsg = setSpellText(c, modeCopy);
+ Common::String ttsMessage;
w.writeString(Common::String::format(Res.SPELLS_FOR,
errorMsg == nullptr ? Res.SPELL_LINES_0_TO_9 : "",
- c->_name.c_str()));
+ c->_name.c_str()), false, &ttsMessage);
// Setup and write out spell list
const char *names[10];
@@ -107,7 +179,27 @@ Character *SpellsDialog::execute(ButtonContainer *priorDialog, Character *c, int
colors[9], names[9],
mode ? Res.SPELL_PTS : Res.GOLD,
mode ? c->_currentSp : party._gold
- ));
+ ), false, &ttsMessage);
+
+#ifdef USE_TTS
+ if (voiceText) {
+ if (errorMsg == nullptr) {
+ speakText(ttsMessage, mode);
+ } else {
+ _vm->sayText(ttsMessage, Common::TextToSpeechManager::INTERRUPT);
+ _buttonTexts.resize(kSpellsSpellCount);
+ _buttonTexts.push_back(selectButton[_vm->_ttsLanguage]);
+ _buttonTexts.push_back(exitButton[_vm->_ttsLanguage]);
+ }
+
+ voiceText = false;
+ }
+
+ if (resetButtons) {
+ resetButtonTexts(ttsMessage, mode);
+ resetButtons = false;
+ }
+#endif
_scrollSprites.draw(0, 4, Common::Point(39, 26));
_scrollSprites.draw(0, 0, Common::Point(187, 26));
@@ -136,10 +228,10 @@ Character *SpellsDialog::execute(ButtonContainer *priorDialog, Character *c, int
c = &party._activeParty[_buttonValue];
spells._lastCaster = _buttonValue;
intf.highlightChar(_buttonValue);
-
+ ttsMessage.clear();
if (_vm->_mode == MODE_INTERACTIVE7) {
windows[10].writeString(Common::String::format(Res.GUILD_OPTIONS,
- XeenEngine::printMil(party._gold).c_str(), Res.GUILD_TEXT, c->_name.c_str()));
+ XeenEngine::printMil(party._gold).c_str(), Res.GUILD_TEXT, c->_name.c_str()), false, &ttsMessage);
} else {
SpellsCategory category = c->getSpellsCategory();
int spellIndex = (c->_currentSpell == -1) ? SPELLS_PER_CLASS : c->_currentSpell;
@@ -148,9 +240,13 @@ Character *SpellsDialog::execute(ButtonContainer *priorDialog, Character *c, int
windows[10].writeString(Common::String::format(Res.CAST_SPELL_DETAILS,
c->_name.c_str(), spells._spellNames[spellId].c_str(),
spells.calcSpellPoints(spellId, c->getCurrentLevel()),
- Res.SPELL_GEM_COST[spellId], c->_currentSp));
+ Res.SPELL_GEM_COST[spellId], c->_currentSp), false, &ttsMessage);
}
+#ifdef USE_TTS
+ voiceText = true;
+ _buttonTexts.clear();
+#endif
if (priorDialog != nullptr)
priorDialog->drawButtons(&windows[0]);
windows[10].update();
@@ -194,6 +290,9 @@ Character *SpellsDialog::execute(ButtonContainer *priorDialog, Character *c, int
if (mode) {
// Casting
selection = newSelection;
+#ifdef USE_TTS
+ _vm->sayText(_buttonTexts[(selection - topIndex) % kSpellsSpellCount], Common::TextToSpeechManager::INTERRUPT);
+#endif
} else {
// Guild spells dialog: Spells Info or Buy
const Common::String &spellName = spells._spellNames[spellId];
@@ -209,6 +308,9 @@ Character *SpellsDialog::execute(ButtonContainer *priorDialog, Character *c, int
sound.stopSound();
intf._overallFrame = 0;
sound.playSound(ccNum ? "parrot2.voc" : "guild12.voc", 1);
+#ifdef USE_TTS
+ resetButtons = true;
+#endif
} else {
sound.playFX(21);
}
@@ -220,23 +322,37 @@ Character *SpellsDialog::execute(ButtonContainer *priorDialog, Character *c, int
case Common::KEYCODE_PAGEUP:
case Common::KEYCODE_KP9:
topIndex = MAX((int)topIndex - 10, 0);
+#ifdef USE_TTS
+ resetButtons = true;
+#endif
break;
case Common::KEYCODE_PAGEDOWN:
case Common::KEYCODE_KP3:
topIndex = MIN(topIndex + 10, (((int)_spells.size() - 1) / 10) * 10);
+#ifdef USE_TTS
+ resetButtons = true;
+#endif
break;
case Common::KEYCODE_UP:
case Common::KEYCODE_KP8:
- if (topIndex > 0)
+ if (topIndex > 0) {
--topIndex;
+#ifdef USE_TTS
+ resetButtons = true;
+#endif
+ }
break;
case Common::KEYCODE_DOWN:
case Common::KEYCODE_KP2:
- if (topIndex < ((int)_spells.size() - 10))
+ if (topIndex < ((int)_spells.size() - 10)) {
++topIndex;
+#ifdef USE_TTS
+ resetButtons = true;
+#endif
+ }
break;
default:
@@ -259,20 +375,20 @@ void SpellsDialog::loadButtons() {
_scrollSprites.load("scroll.icn");
addButton(Common::Rect(187, 26, 198, 36), Common::KEYCODE_UP, &_scrollSprites);
addButton(Common::Rect(187, 111, 198, 121), Common::KEYCODE_DOWN, &_scrollSprites);
- addButton(Common::Rect(40, 28, 187, 36), Common::KEYCODE_1);
- addButton(Common::Rect(40, 37, 187, 45), Common::KEYCODE_2);
- addButton(Common::Rect(40, 46, 187, 54), Common::KEYCODE_3);
- addButton(Common::Rect(40, 55, 187, 63), Common::KEYCODE_4);
- addButton(Common::Rect(40, 64, 187, 72), Common::KEYCODE_5);
- addButton(Common::Rect(40, 73, 187, 81), Common::KEYCODE_6);
- addButton(Common::Rect(40, 82, 187, 90), Common::KEYCODE_7);
- addButton(Common::Rect(40, 91, 187, 99), Common::KEYCODE_8);
- addButton(Common::Rect(40, 100, 187, 108), Common::KEYCODE_9);
- addButton(Common::Rect(40, 109, 187, 117), Common::KEYCODE_0);
- addButton(Common::Rect(174, 123, 198, 133), Common::KEYCODE_ESCAPE);
+ addButton(Common::Rect(40, 28, 187, 36), Common::KEYCODE_1, nullptr, kSpellsSpell1);
+ addButton(Common::Rect(40, 37, 187, 45), Common::KEYCODE_2, nullptr, kSpellsSpell2);
+ addButton(Common::Rect(40, 46, 187, 54), Common::KEYCODE_3, nullptr, kSpellsSpell3);
+ addButton(Common::Rect(40, 55, 187, 63), Common::KEYCODE_4, nullptr, kSpellsSpell4);
+ addButton(Common::Rect(40, 64, 187, 72), Common::KEYCODE_5, nullptr, kSpellsSpell5);
+ addButton(Common::Rect(40, 73, 187, 81), Common::KEYCODE_6, nullptr, kSpellsSpell6);
+ addButton(Common::Rect(40, 82, 187, 90), Common::KEYCODE_7, nullptr, kSpellsSpell7);
+ addButton(Common::Rect(40, 91, 187, 99), Common::KEYCODE_8, nullptr, kSpellsSpell8);
+ addButton(Common::Rect(40, 100, 187, 108), Common::KEYCODE_9, nullptr, kSpellsSpell9);
+ addButton(Common::Rect(40, 109, 187, 117), Common::KEYCODE_0, nullptr, kSpellsSpell10);
+ addButton(Common::Rect(174, 123, 198, 133), Common::KEYCODE_ESCAPE, nullptr, kSpellsExit);
addButton(Common::Rect(187, 35, 198, 73), Common::KEYCODE_PAGEUP);
addButton(Common::Rect(187, 74, 198, 112), Common::KEYCODE_PAGEDOWN);
- addButton(Common::Rect(132, 123, 168, 133), Common::KEYCODE_s);
+ addButton(Common::Rect(132, 123, 168, 133), Common::KEYCODE_s, nullptr, kSpellsSelect);
addPartyButtons(_vm);
}
@@ -404,6 +520,53 @@ const char *SpellsDialog::setSpellText(Character *c, int mode) {
return _spells.empty() ? Res.SPELLS_LEARNED_ALL : nullptr;
}
+#ifdef USE_TTS
+
+void SpellsDialog::speakText(const Common::String &text, int mode) {
+ _vm->sayText(resetButtonTexts(text, mode));
+}
+
+Common::String SpellsDialog::resetButtonTexts(const Common::String &text, int mode) {
+ uint index = 0;
+
+ // Get the numbers 0 - 9
+ Common::String spellList[kSpellsSpellCount];
+ for (uint8 i = 0; i < kSpellsSpellCount; ++i) {
+ spellList[i] = getNextTextSection(text, index);
+ }
+
+ // "Spells for" message
+ Common::String combinedText = getNextTextSection(text, index);
+
+ _buttonTexts.clear();
+ // Spell names and their costs
+ uint spellCount = MIN((uint)kSpellsSpellCount, _spells.size());
+ for (uint i = 0; i < spellCount; ++i) {
+ spellList[i] += ": " + getNextTextSection(text, index, 2, ": ");
+ _buttonTexts.push_back(spellList[i]);
+ combinedText += spellList[i];
+ }
+
+ // Numbers for empty spells
+ for (uint i = spellCount; i < kSpellsSpellCount; ++i) {
+ _buttonTexts.push_back(spellList[i]);
+ }
+
+ if (mode) {
+ _buttonTexts.push_back(selectButton[_vm->_ttsLanguage]);
+ } else {
+ _buttonTexts.push_back("");
+ }
+ _buttonTexts.push_back(exitButton[_vm->_ttsLanguage]);
+
+ // Spell points/gold
+ combinedText += getNextTextSection(text, index);
+
+ return combinedText;
+}
+
+#endif
+
/*------------------------------------------------------------------------*/
CastSpell::CastSpell(XeenEngine *vm) : ButtonContainer(vm) {
@@ -478,6 +641,11 @@ int CastSpell::execute(Character *&c) {
int spellId = -1;
bool redrawFlag = true;
+
+#ifdef USE_TTS
+ _vm->stopTextToSpeech();
+#endif
+
do {
if (redrawFlag) {
SpellsCategory category = c->getSpellsCategory();
@@ -487,9 +655,13 @@ int CastSpell::execute(Character *&c) {
int gemCost = Res.SPELL_GEM_COST[spellId];
int spCost = spells.calcSpellPoints(spellId, c->getCurrentLevel());
+ Common::String ttsMessage;
w.writeString(Common::String::format(Res.CAST_SPELL_DETAILS,
c->_name.c_str(), spells._spellNames[spellId].c_str(),
- spCost, gemCost, c->_currentSp));
+ spCost, gemCost, c->_currentSp), false, &ttsMessage);
+#ifdef USE_TTS
+ speakText(ttsMessage);
+#endif
drawButtons(&windows[0]);
w.update();
@@ -547,13 +719,24 @@ int CastSpell::execute(Character *&c) {
void CastSpell::loadButtons() {
_iconSprites.load("cast.icn");
- addButton(Common::Rect(234, 108, 259, 128), Res.KeyConstants.DialogsSpells.KEY_CAST, &_iconSprites);
- addButton(Common::Rect(261, 108, 285, 128), Res.KeyConstants.DialogsSpells.KEY_NEW, &_iconSprites);
+ addButton(Common::Rect(234, 108, 259, 128), Res.KeyConstants.DialogsSpells.KEY_CAST, &_iconSprites, kCastSpellCast);
+ addButton(Common::Rect(261, 108, 285, 128), Res.KeyConstants.DialogsSpells.KEY_NEW, &_iconSprites, kCastSpellNew);
- addButton(Common::Rect(288, 108, 312, 128), Common::KEYCODE_ESCAPE, &_iconSprites);
+ addButton(Common::Rect(288, 108, 312, 128), Common::KEYCODE_ESCAPE, &_iconSprites, kCastSpellExit);
addPartyButtons(_vm);
}
+#ifdef USE_TTS
+
+void CastSpell::speakText(const Common::String &text) {
+ uint index = 0;
+ Common::String buttonText = addNextTextToButtons(text, index, kSpellsButtonCount);
+ _vm->sayText(text.substr(index + 1), Common::TextToSpeechManager::INTERRUPT);
+ _vm->sayText(buttonText);
+}
+
+#endif
+
/*------------------------------------------------------------------------*/
Character *SpellOnWho::show(XeenEngine *vm, int spellId) {
@@ -654,16 +837,21 @@ int SelectElement::execute(int spellId) {
loadButtons();
w.open();
- w.writeString(Res.WHICH_ELEMENT1);
+ Common::String ttsMessage;
+ w.writeString(Res.WHICH_ELEMENT1, false, &ttsMessage);
drawButtons(&windows[0]);
w.update();
+#ifdef USE_TTS
+ speakText(ttsMessage);
+#endif
+
while (result == 999) {
do {
events.updateGameCounter();
intf.draw3d(true, false);
w.frame();
- w.writeString(Res.WHICH_ELEMENT2);
+ w.writeString(Res.WHICH_ELEMENT2, false);
drawButtons(&windows[0]);
w.update();
@@ -698,12 +886,22 @@ int SelectElement::execute(int spellId) {
void SelectElement::loadButtons() {
_iconSprites.load("element.icn");
- addButton(Common::Rect(60, 92, 84, 112), Res.KeyConstants.DialogsSpells.KEY_FIRE, &_iconSprites);
- addButton(Common::Rect(90, 92, 114, 112), Res.KeyConstants.DialogsSpells.KEY_ELEC, &_iconSprites);
- addButton(Common::Rect(120, 92, 144, 112), Res.KeyConstants.DialogsSpells.KEY_COLD, &_iconSprites);
- addButton(Common::Rect(150, 92, 174, 112), Res.KeyConstants.DialogsSpells.KEY_ACID, &_iconSprites);
+ addButton(Common::Rect(60, 92, 84, 112), Res.KeyConstants.DialogsSpells.KEY_FIRE, &_iconSprites, kSelectElementFire);
+ addButton(Common::Rect(90, 92, 114, 112), Res.KeyConstants.DialogsSpells.KEY_ELEC, &_iconSprites, kSelectElementElectricity);
+ addButton(Common::Rect(120, 92, 144, 112), Res.KeyConstants.DialogsSpells.KEY_COLD, &_iconSprites, kSelectElementCold);
+ addButton(Common::Rect(150, 92, 174, 112), Res.KeyConstants.DialogsSpells.KEY_ACID, &_iconSprites, kSelectElementAcid);
+}
+
+#ifdef USE_TTS
+
+void SelectElement::speakText(const Common::String &text) {
+ uint index = 0;
+ _vm->sayText(getNextTextSection(text, index), Common::TextToSpeechManager::INTERRUPT);
+ _vm->sayText(addNextTextToButtons(text, index, kSelectElementButtonCount));
}
+#endif
+
/*------------------------------------------------------------------------*/
void NotWhileEngaged::show(XeenEngine *vm, int spellId) {
@@ -773,13 +971,18 @@ bool LloydsBeacon::execute() {
// Get the destination map name
Common::String mapName = Map::getMazeName(c._lloydMap, c._lloydSide);
+ Common::String ttsMessage;
// Display the dialog
w.open();
w.writeString(Common::String::format(Res.LLOYDS_BEACON, mapName.c_str(),
- c._lloydPosition.x, c._lloydPosition.y));
+ c._lloydPosition.x, c._lloydPosition.y), true, &ttsMessage);
drawButtons(&w);
w.update();
+#ifdef USE_TTS
+ speakText(ttsMessage);
+#endif
+
bool result = true;
do {
do {
@@ -829,10 +1032,20 @@ bool LloydsBeacon::execute() {
void LloydsBeacon::loadButtons() {
_iconSprites.load("lloyds.icn");
- addButton(Common::Rect(281, 108, 305, 128), Res.KeyConstants.DialogsSpells.KEY_RETURN, &_iconSprites);
- addButton(Common::Rect(242, 108, 266, 128), Res.KeyConstants.DialogsSpells.KEY_SET, &_iconSprites);
+ addButton(Common::Rect(281, 108, 305, 128), Res.KeyConstants.DialogsSpells.KEY_RETURN, &_iconSprites, kLloydsBeaconReturn);
+ addButton(Common::Rect(242, 108, 266, 128), Res.KeyConstants.DialogsSpells.KEY_SET, &_iconSprites, kLloydsBeaconSet);
}
+#ifdef USE_TTS
+
+void LloydsBeacon::speakText(const Common::String &text) {
+ uint index = 0;
+ getNextTextSection(text, index, kLloydsBeaconInfoCount);
+ addNextTextToButtons(text, index, kLloydsBeaconButtonCount);
+}
+
+#endif
+
/*------------------------------------------------------------------------*/
int Teleport::show(XeenEngine *vm) {
@@ -988,10 +1201,15 @@ void IdentifyMonster::execute() {
sound.playFX(20);
w.open();
+ Common::String ttsMessage;
w.writeString(Common::String::format(Res.IDENTIFY_MONSTERS,
- monsterDesc[0].c_str(), monsterDesc[1].c_str(), monsterDesc[2].c_str()));
+ monsterDesc[0].c_str(), monsterDesc[1].c_str(), monsterDesc[2].c_str()), false, &ttsMessage);
w.update();
+#ifdef USE_TTS
+ speakText(ttsMessage);
+#endif
+
do {
events.updateGameCounter();
intf.draw3d(false, false);
@@ -1004,6 +1222,27 @@ void IdentifyMonster::execute() {
w.close();
}
+#ifdef USE_TTS
+
+void IdentifyMonster::speakText(const Common::String &text) const {
+ Common::String headers[kIdentifyMonsterHeaderCount];
+ uint index = 0;
+ for (uint8 i = 0; i < kIdentifyMonsterHeaderCount; ++i) {
+ headers[i] = getNextTextSection(text, index);
+ }
+
+ Combat &combat = *_vm->_combat;
+ for (uint8 i = 0; i < kIdentifyMonsterMaxMonsterCount; ++i) {
+ if (combat._attackMonsters[i] == -1)
+ continue;
+
+ for (int j = 0; j < kIdentifyMonsterHeaderCount; ++j) {
+ _vm->sayText(headers[j] + ": " + getNextTextSection(text, index));
+ }
+ }
+}
+
+#endif
/*------------------------------------------------------------------------*/
diff --git a/engines/mm/xeen/dialogs/dialogs_spells.h b/engines/mm/xeen/dialogs/dialogs_spells.h
index fdb497afc7d..b2f68ff8f20 100644
--- a/engines/mm/xeen/dialogs/dialogs_spells.h
+++ b/engines/mm/xeen/dialogs/dialogs_spells.h
@@ -72,6 +72,23 @@ private:
* Sets the spell text
*/
const char *setSpellText(Character *c, int isCasting);
+
+#ifdef USE_TTS
+ /**
+ * Voices text with TTS and sets up buttons
+ * @param text Text for voicing and buttons. Each section should be separated by a newline
+ * @param mode Mode of the spells menu
+ */
+ void speakText(const Common::String &text, int mode);
+
+ /**
+ * Resets all button texts to what's currently on screen
+ * @param text Text, both for buttons and for voicing. Each section should be separated by a newline
+ * @param mode Mode of the spells menu
+ * @returns All text combined
+ */
+ Common::String resetButtonTexts(const Common::String &text, int mode);
+#endif
public:
/**
* Show the spells list dialog
@@ -91,6 +108,10 @@ private:
int execute(Character *&c);
void loadButtons();
+
+#ifdef USE_TTS
+ void speakText(const Common::String &text);
+#endif
public:
static int show(XeenEngine *vm);
};
@@ -113,6 +134,10 @@ private:
int execute(int spellId);
void loadButtons();
+
+#ifdef USE_TTS
+ void speakText(const Common::String &text);
+#endif
public:
static int show(XeenEngine *vm, int spellId);
};
@@ -135,6 +160,10 @@ private:
bool execute();
void loadButtons();
+
+#ifdef USE_TTS
+ void speakText(const Common::String &text);
+#endif
public:
static bool show(XeenEngine *vm);
};
@@ -164,6 +193,10 @@ private:
IdentifyMonster(XeenEngine *vm) : ButtonContainer(vm) {}
void execute();
+
+#ifdef USE_TTS
+ void speakText(const Common::String &text) const;
+#endif
public:
static void show(XeenEngine *vm);
};
diff --git a/engines/mm/xeen/dialogs/dialogs_whowill.cpp b/engines/mm/xeen/dialogs/dialogs_whowill.cpp
index 5549c6297d0..26a34fa770a 100644
--- a/engines/mm/xeen/dialogs/dialogs_whowill.cpp
+++ b/engines/mm/xeen/dialogs/dialogs_whowill.cpp
@@ -56,9 +56,14 @@ int WhoWill::execute(int message, int action, bool type) {
Res.WHO_ACTIONS[message], party._activeParty.size());
windows[36].open();
- windows[36].writeString(msg);
+ Common::String ttsMessage;
+ windows[36].writeString(msg, false, &ttsMessage);
windows[36].update();
+#ifdef USE_TTS
+ speakText(ttsMessage);
+#endif
+
intf._face1State = map._headData[party._mazePosition.y][party._mazePosition.x]._left;
intf._face2State = map._headData[party._mazePosition.y][party._mazePosition.x]._right;
@@ -97,10 +102,29 @@ int WhoWill::execute(int message, int action, bool type) {
}
}
+#ifdef USE_TTS
+ _vm->stopTextToSpeech();
+#endif
intf._face1State = intf._face2State = 2;
windows[36].close();
return _buttonValue;
}
+#ifdef USE_TTS
+
+void WhoWill::speakText(const Common::String &text) const {
+ uint index = 0;
+ // Title
+ _vm->sayText(getNextTextSection(text, index));
+
+ // "Who will" and the action verb (i.e. try)
+ _vm->sayText(getNextTextSection(text, index, 2, " "));
+
+ // F1 - F6
+ _vm->sayText(getNextTextSection(text, index));
+}
+
+#endif
+
} // End of namespace Xeen
} // End of namespace MM
diff --git a/engines/mm/xeen/dialogs/dialogs_whowill.h b/engines/mm/xeen/dialogs/dialogs_whowill.h
index 39d742b228b..7ad6470aeca 100644
--- a/engines/mm/xeen/dialogs/dialogs_whowill.h
+++ b/engines/mm/xeen/dialogs/dialogs_whowill.h
@@ -32,6 +32,10 @@ private:
WhoWill(XeenEngine *vm) : ButtonContainer(vm) {}
int execute(int message, int action, bool type);
+
+#ifdef USE_TTS
+ void speakText(const Common::String &text) const;
+#endif
public:
static int show(XeenEngine *vm, int message, int action, bool type);
};
diff --git a/engines/mm/xeen/events.cpp b/engines/mm/xeen/events.cpp
index 160edc45cb0..e7b00ff2739 100644
--- a/engines/mm/xeen/events.cpp
+++ b/engines/mm/xeen/events.cpp
@@ -89,6 +89,9 @@ void EventsManager::pollEvents() {
break;
case Common::EVENT_MOUSEMOVE:
_mousePos = event.mouse;
+#ifdef USE_TTS
+ _vm->_mouseMoved = true;
+#endif
break;
case Common::EVENT_LBUTTONDOWN:
_mousePressed = true;
diff --git a/engines/mm/xeen/font.cpp b/engines/mm/xeen/font.cpp
index 91fac408226..d4f0fd91fa2 100644
--- a/engines/mm/xeen/font.cpp
+++ b/engines/mm/xeen/font.cpp
@@ -109,10 +109,18 @@ bool FontSurface::isSpace(char c) {
return (c & 0x7f) == ' ';
}
-const char *FontSurface::writeString(const Common::String &s, const Common::Rect &bounds) {
+const char *FontSurface::writeString(const Common::String &s, const Common::Rect &bounds, bool ttsVoiceText, Common::String *ttsMessage) {
_displayString = s.c_str();
assert(_fontData);
+#ifdef USE_TTS
+ bool deleteTTSMessage = false;
+ if (!ttsMessage) {
+ ttsMessage = new Common::String();
+ deleteTTSMessage = true;
+ }
+#endif
+
for (;;) {
const char *msgStartP = _displayString;
_msgWraps = false;
@@ -198,10 +206,16 @@ const char *FontSurface::writeString(const Common::String &s, const Common::Rect
if (c == ' ') {
_writePos.x += _fontReduced ? 3 : 4;
+#ifdef USE_TTS
+ *ttsMessage += ' ';
+#endif
} else if (c == '\r') {
fillRect(bounds, _bgColor);
addDirtyRect(bounds);
_writePos = Common::Point(bounds.left, bounds.top);
+#ifdef USE_TTS
+ *ttsMessage += '\n';
+#endif
} else if (c == 1) {
// Turn off reduced font mode
_fontReduced = false;
@@ -231,6 +245,9 @@ const char *FontSurface::writeString(const Common::String &s, const Common::Rect
} else if (c == 6) {
// Non-breakable space
writeChar(' ', bounds);
+#ifdef USE_TTS
+ *ttsMessage += ' ';
+#endif
} else if (c == 7) {
// Set text background color
int bgColor = fontAtoi();
@@ -270,7 +287,14 @@ const char *FontSurface::writeString(const Common::String &s, const Common::Rect
// Skip x position
int xAmount = fontAtoi();
_writePos.x = MIN(bounds.left + xAmount, (int)bounds.right);
+#ifdef USE_TTS
+ *ttsMessage += '\n';
+#endif
} else if (c == 10) {
+#ifdef USE_TTS
+ *ttsMessage += '\n';
+#endif
+
// Newline
if (newLine(bounds))
return _displayString;
@@ -278,6 +302,9 @@ const char *FontSurface::writeString(const Common::String &s, const Common::Rect
// Set y position
int yp = fontAtoi();
_writePos.y = MIN(bounds.top + yp, (int)bounds.bottom);
+#ifdef USE_TTS
+ *ttsMessage += '\n';
+#endif
} else if (c == 12) {
// Set text colors
int idx = fontAtoi(2);
@@ -286,13 +313,22 @@ const char *FontSurface::writeString(const Common::String &s, const Common::Rect
setTextColor(idx);
} else if (Common::RU_RUS == lang && (c & 0x80)) {
writeChar(c, bounds);
+#ifdef USE_TTS
+ *ttsMessage += c;
+#endif
} else if (c < ' ') {
// End of string or invalid command
_displayString = nullptr;
+#ifdef USE_TTS
+ *ttsMessage += '\n';
+#endif
break;
} else {
// Standard character - write it out
writeChar(c, bounds);
+#ifdef USE_TTS
+ *ttsMessage += c;
+#endif
}
}
@@ -303,6 +339,17 @@ const char *FontSurface::writeString(const Common::String &s, const Common::Rect
break;
}
+#ifdef USE_TTS
+ if (ttsVoiceText) {
+ g_vm->sayText(*ttsMessage);
+ }
+
+ if (deleteTTSMessage) {
+ delete ttsMessage;
+ ttsMessage = nullptr;
+ }
+#endif
+
return _displayString;
}
diff --git a/engines/mm/xeen/font.h b/engines/mm/xeen/font.h
index b8b33605c2e..850185148fa 100644
--- a/engines/mm/xeen/font.h
+++ b/engines/mm/xeen/font.h
@@ -108,13 +108,15 @@ public:
/**
* Write a string to the surface
- * @param s String to display
- * @param clipRect Window bounds to display string within
- * @returns Any string remainder that couldn't be displayed
+ * @param s String to display
+ * @param clipRect Window bounds to display string within
+ * @param ttsVoiceText Whether to voice the string with TTS
+ * @param ttsMessage Message for TTS, which will be built
+ * @returns Any string remainder that couldn't be displayed
* @remarks Note that bounds is just used for wrapping purposes. Unless
* justification is set, the message will be written at _writePos
*/
- const char *writeString(const Common::String &s, const Common::Rect &clipRect);
+ const char *writeString(const Common::String &s, const Common::Rect &clipRect, bool ttsVoiceText = true, Common::String *ttsMessage = nullptr);
bool isSpace(char c);
/**
* Write a charcter to the window
diff --git a/engines/mm/xeen/interface.cpp b/engines/mm/xeen/interface.cpp
index e6685cfc8eb..c90c9122745 100644
--- a/engines/mm/xeen/interface.cpp
+++ b/engines/mm/xeen/interface.cpp
@@ -40,10 +40,22 @@
namespace MM {
namespace Xeen {
+#ifdef USE_TTS
+
+static const uint8 kCombatButtonCount = 3;
+
+#endif
+
enum {
SCENE_WINDOW = 11, SCENE_WIDTH = 216, SCENE_HEIGHT = 132
};
+enum CombatButtonTTSTextIndex {
+ kCombatMonster1 = 0,
+ kCombatMonster2 = 1,
+ kCombatMonster3 = 2
+};
+
PartyDrawer::PartyDrawer(XeenEngine *vm): _vm(vm) {
_restoreSprites.load("restorex.icn");
_hpSprites.load("hpbars.icn");
@@ -337,9 +349,9 @@ void Interface::setMainButtons(IconsMode mode) {
addButton(Common::Rect(260, 169, 284, 189), Common::KEYCODE_DOWN, spr);
addButton(Common::Rect(286, 169, 310, 189), (Common::KBD_CTRL << 16) | Common::KEYCODE_RIGHT, spr);
addButton(Common::Rect(236, 11, 308, 69), Common::KEYCODE_EQUALS);
- addButton(Common::Rect(239, 27, 312, 37), Common::KEYCODE_1);
- addButton(Common::Rect(239, 37, 312, 47), Common::KEYCODE_2);
- addButton(Common::Rect(239, 47, 312, 57), Common::KEYCODE_3);
+ addButton(Common::Rect(239, 27, 312, 37), Common::KEYCODE_1, nullptr, kCombatMonster1);
+ addButton(Common::Rect(239, 37, 312, 47), Common::KEYCODE_2, nullptr, kCombatMonster2);
+ addButton(Common::Rect(239, 47, 312, 57), Common::KEYCODE_3, nullptr, kCombatMonster3);
addPartyButtons(_vm);
if (mode == ICONS_COMBAT) {
@@ -1357,7 +1369,10 @@ void Interface::draw3d(bool updateFlag, bool pauseFlag) {
// Draw any on-screen text if flagged to do so
if (_upDoorText && combat._attackMonsters[0] == -1) {
- windows[3].writeString(_screenText);
+ windows[3].writeString(_screenText, _screenText != _ttsPreviousScreenText);
+ _ttsPreviousScreenText = _screenText;
+ } else {
+ _ttsPreviousScreenText.clear();
}
if (updateFlag) {
@@ -1627,6 +1642,11 @@ void Interface::doCombat() {
Window &w = windows[2];
w.open();
bool breakFlag = false;
+ Common::String ttsMessage;
+ bool ttsVoiceText = true;
+#ifdef USE_TTS
+ uint ttsIndex = 0;
+#endif
while (!_vm->shouldExit() && !breakFlag && !party._dead && _vm->_mode == MODE_COMBAT) {
// FIXME: I've had a rare issue where the loop starts with a non-party _whosTurn. Unfortunately,
@@ -1638,12 +1658,22 @@ void Interface::doCombat() {
highlightChar(combat._whosTurn);
combat.setSpeedTable();
+ ttsMessage.clear();
// Write out the description of the monsters being battled
- w.writeString(combat.getMonsterDescriptions());
+ w.writeString(combat.getMonsterDescriptions(), ttsVoiceText, &ttsMessage);
+ ttsVoiceText = false;
+
_combatIcons.draw(0, 32, Common::Point(233, combat._attackDurationCtr * 10 + 27),
SPRFLAG_800, 0);
w.update();
+#ifdef USE_TTS
+ ttsIndex = 0;
+ getNextTextSection(ttsMessage, ttsIndex); // "Combat"
+ _buttonTexts.clear();
+ addNextTextToButtons(ttsMessage, ttsIndex, kCombatButtonCount);
+#endif
+
// Wait for keypress
index = 0;
do {
@@ -1682,6 +1712,9 @@ void Interface::doCombat() {
if (combat._attackMonsters[_buttonValue] != -1) {
combat._monster2Attack = combat._attackMonsters[_buttonValue];
combat._attackDurationCtr = _buttonValue;
+#ifdef USE_TTS
+ _vm->sayText(_buttonTexts[_buttonValue], Common::TextToSpeechManager::INTERRUPT);
+#endif
}
break;
diff --git a/engines/mm/xeen/interface.h b/engines/mm/xeen/interface.h
index eda151c6a08..c65e17f13f1 100644
--- a/engines/mm/xeen/interface.h
+++ b/engines/mm/xeen/interface.h
@@ -175,6 +175,7 @@ public:
int _levitateUIFrame;
bool _upDoorText;
Common::String _screenText;
+ Common::String _ttsPreviousScreenText;
byte _tillMove;
int _charFX[6];
IconsMode _iconsMode;
diff --git a/engines/mm/xeen/locations.cpp b/engines/mm/xeen/locations.cpp
index 79b5041f06c..1e5052362b0 100644
--- a/engines/mm/xeen/locations.cpp
+++ b/engines/mm/xeen/locations.cpp
@@ -34,6 +34,57 @@ namespace MM {
namespace Xeen {
namespace Locations {
+#ifdef USE_TTS
+ static const uint8 kBankButtonCount = 3;
+
+ static const uint8 kTavernOptionsCount = 4;
+ static const uint8 kTavernButtonCount = 2;
+
+ static const uint8 kTempleOptionsCount = 3;
+
+ static const uint8 kTrainingButtonCount = 2;
+ static const uint8 kTrainingIneligibleInfoCount = 2;
+ static const uint8 kTrainingEligibleInfoCount = 4;
+#endif
+
+enum BankButtonTTSTextIndex {
+ kBankDeposit = 0,
+ kBankWithdraw = 1,
+ kBankEscape = 2
+};
+
+enum BlacksmithButtonTTSTextIndex {
+ kBlacksmithBrowse = 0,
+ kBlacksmithExit = 1
+};
+
+enum GuildButtonTTSTextIndex {
+ kGuildBuySpells = 0,
+ kGuildSpellInfo = 1,
+ kGuildExit = 2
+};
+
+enum TavernButtonTTSTextIndex {
+ kTavernDrink = 0,
+ kTavernFood = 1,
+ kTavernTip = 2,
+ kTavernRumors = 3,
+ kTavernSignIn = 4,
+ kTavernExit = 5
+};
+
+enum TempleButtonTTSTextIndex {
+ kTempleHeal = 0,
+ kTempleDonation = 1,
+ kTempleUncurse = 2,
+ kTempleExit = 3
+};
+
+enum TrainingButtonTTSTextIndex {
+ kTrainingTrain = 0,
+ kTrainingExit = 1
+};
+
BaseLocation::BaseLocation(LocationAction action) : ButtonContainer(g_vm),
_locationActionId(action), _ccNum(g_vm->_files->_ccNum),
_vocName("hello1.voc"), _exitToUi(false) {
@@ -48,6 +99,10 @@ BaseLocation::BaseLocation(LocationAction action) : ButtonContainer(g_vm),
_farewellTime = 0;
_drawCtr1 = _drawCtr2 = 0;
_animPos = Common::Point(8, 8);
+ _ttsVoiceText = false;
+#ifdef USE_TTS
+ _resetText = false;
+#endif
}
BaseLocation::~BaseLocation() {
@@ -92,8 +147,19 @@ int BaseLocation::show() {
if (_vm->shouldExit() || _exitToUi)
return 0;
+ Common::String ttsMessage;
Common::String msg = createLocationText(*charP);
- windows[10].writeString(msg);
+ windows[10].writeString(msg, _ttsVoiceText, &ttsMessage);
+ _ttsVoiceText = false;
+
+#ifdef USE_TTS
+ if (_resetText) {
+ _buttonTexts.clear();
+ speakTextAndButtons(ttsMessage);
+ _resetText = false;
+ }
+#endif
+
drawButtons(&windows[0]);
} while (_buttonValue != Common::KEYCODE_ESCAPE);
@@ -135,8 +201,12 @@ void BaseLocation::drawWindow() {
// Open up the window and write the string
intf.assembleBorder();
windows[10].open();
- windows[10].writeString(title);
+ Common::String ttsMessage;
+ windows[10].writeString(title, false, &ttsMessage);
drawButtons(&windows[0]);
+#ifdef USE_TTS
+ speakTextAndButtons(ttsMessage);
+#endif
windows[0].update();
intf.highlightChar(0);
@@ -303,9 +373,9 @@ int BaseLocation::wait() {
BankLocation::BankLocation() : BaseLocation(BANK) {
_icons1.load("bank.icn");
_icons2.load("bank2.icn");
- addButton(Common::Rect(234, 108, 259, 128), Res.KeyConstants.Locations.KEY_DEP, &_icons1);
- addButton(Common::Rect(261, 108, 285, 128), Res.KeyConstants.Locations.KEY_WITH, &_icons1);
- addButton(Common::Rect(288, 108, 312, 128), Common::KEYCODE_ESCAPE, &_icons1);
+ addButton(Common::Rect(234, 108, 259, 128), Res.KeyConstants.Locations.KEY_DEP, &_icons1, kBankDeposit);
+ addButton(Common::Rect(261, 108, 285, 128), Res.KeyConstants.Locations.KEY_WITH, &_icons1, kBankWithdraw);
+ addButton(Common::Rect(288, 108, 312, 128), Common::KEYCODE_ESCAPE, &_icons1, kBankEscape);
_animFrame = 1;
_vocName = _ccNum ? "bank1.voc" : "banker.voc";
@@ -372,10 +442,16 @@ void BankLocation::depositWithdrawl(PartyBank whereId) {
XeenEngine::printMil(gems).c_str());
w.open();
- w.writeString(msg);
+ Common::String ttsMessage;
+ w.writeString(msg, false, &ttsMessage);
drawButtons(&w);
w.update();
+#ifdef USE_TTS
+ Common::String oldButtonTexts[kBankButtonCount];
+ speakDepositWithdrawalText(ttsMessage, oldButtonTexts);
+#endif
+
sound.stopSound();
File voc("coina.voc");
ConsumableType consType = CONS_GOLD;
@@ -437,8 +513,12 @@ void BankLocation::depositWithdrawl(PartyBank whereId) {
}
} while (!g_vm->shouldExit());
- for (uint idx = 0; idx < _buttons.size(); ++idx)
+ for (uint idx = 0; idx < _buttons.size(); ++idx) {
_buttons[idx]._sprites = &_icons1;
+#ifdef USE_TTS
+ _buttonTexts[idx] = oldButtonTexts[idx];
+#endif
+ }
_buttons[0]._value = Res.KeyConstants.Locations.KEY_DEP;
_buttons[1]._value = Res.KeyConstants.Locations.KEY_WITH;
_buttons[2]._value = Common::KEYCODE_ESCAPE;
@@ -447,13 +527,60 @@ void BankLocation::depositWithdrawl(PartyBank whereId) {
clearEvents();
}
+#ifdef USE_TTS
+
+void BankLocation::speakTextAndButtons(const Common::String &text) {
+ uint index = 0;
+
+ // Dep/With/ESC buttons
+ Common::String buttonTexts = addNextTextToButtons(text, index, kBankButtonCount);
+
+ // Header ("Bank of ..." and "Bank")
+ _vm->sayText(getNextTextSection(text, index, 2));
+
+ // Bank gold and gems
+ for (uint8 i = 0; i < 2; ++i) {
+ _vm->sayText(getNextTextSection(text, index, 2, ": "));
+ }
+
+ // Header ("Party")
+ _vm->sayText(getNextTextSection(text, index));
+
+ // Party gold and gems
+ for (uint8 i = 0; i < 2; ++i) {
+ _vm->sayText(getNextTextSection(text, index, 2, ": "));
+ }
+
+ _vm->sayText(buttonTexts);
+}
+
+void BankLocation::speakDepositWithdrawalText(const Common::String &text, Common::String oldButtonTexts[]) {
+ uint index = 0;
+ // Title
+ _vm->sayText(getNextTextSection(text, index), Common::TextToSpeechManager::INTERRUPT);
+
+ // Gold and gems
+ for (uint8 i = 0; i < 2; ++i) {
+ _vm->sayText(getNextTextSection(text, index, 2, ": "));
+ }
+
+ // New buttons
+ for (uint i = 0; i < _buttonTexts.size(); ++i) {
+ oldButtonTexts[i] = _buttonTexts[i];
+ _buttonTexts[i] = getNextTextSection(text, index);
+ _vm->sayText(_buttonTexts[i]);
+ }
+}
+
+#endif
+
/*------------------------------------------------------------------------*/
BlacksmithLocation::BlacksmithLocation() : BaseLocation(BLACKSMITH) {
_icons1.load("esc.icn");
- addButton(Common::Rect(261, 108, 285, 128), Common::KEYCODE_ESCAPE, &_icons1);
+ addButton(Common::Rect(261, 108, 285, 128), Common::KEYCODE_ESCAPE, &_icons1, kBlacksmithExit);
addButton(Common::Rect(234, 54, 308, 62), 0);
- addButton(Common::Rect(234, 64, 308, 72), Res.KeyConstants.Locations.KEY_BROWSE);
+ addButton(Common::Rect(234, 64, 308, 72), Res.KeyConstants.Locations.KEY_BROWSE, nullptr, kBlacksmithBrowse);
addButton(Common::Rect(234, 74, 308, 82), 0);
addButton(Common::Rect(234, 84, 308, 92), 0);
@@ -475,6 +602,9 @@ Character *BlacksmithLocation::doOptions(Character *c) {
_buttonValue -= Common::KEYCODE_F1;
if (_buttonValue < (int)party._activeParty.size()) {
c = &party._activeParty[_buttonValue];
+#ifdef USE_TTS
+ _vm->sayText(c->_name, Common::TextToSpeechManager::INTERRUPT);
+#endif
intf.highlightChar(_buttonValue);
}
} else if (_buttonValue == Res.KeyConstants.Locations.KEY_BROWSE) {
@@ -497,15 +627,34 @@ void BlacksmithLocation::farewell() {
}
}
+#ifdef USE_TTS
+
+void BlacksmithLocation::speakTextAndButtons(const Common::String &text) {
+ uint index = 0;
+ // Header ("Store options for" and character name)
+ _vm->sayText(getNextTextSection(text, index, 2, " "));
+
+ // Browse option
+ _vm->sayText(addNextTextToButtons(text, index));
+
+ // Gold
+ _vm->sayText(getNextTextSection(text, index, 2, ": "));
+
+ // ESC button
+ _vm->sayText(addNextTextToButtons(text, index));
+}
+
+#endif
+
/*------------------------------------------------------------------------*/
GuildLocation::GuildLocation() : BaseLocation(GUILD) {
loadStrings("spldesc.bin");
_icons1.load("esc.icn");
- addButton(Common::Rect(261, 108, 285, 128), Common::KEYCODE_ESCAPE, &_icons1);
+ addButton(Common::Rect(261, 108, 285, 128), Common::KEYCODE_ESCAPE, &_icons1, kGuildExit);
addButton(Common::Rect(234, 54, 308, 62), 0);
- addButton(Common::Rect(234, 64, 308, 72), Res.KeyConstants.Locations.KEY_BUY_SPELLS);
- addButton(Common::Rect(234, 74, 308, 82), Res.KeyConstants.Locations.KEY_SPELL_INFO);
+ addButton(Common::Rect(234, 64, 308, 72), Res.KeyConstants.Locations.KEY_BUY_SPELLS, nullptr, kGuildBuySpells);
+ addButton(Common::Rect(234, 74, 308, 82), Res.KeyConstants.Locations.KEY_SPELL_INFO, nullptr, kGuildSpellInfo);
addButton(Common::Rect(234, 84, 308, 92), 0);
g_vm->_mode = MODE_INTERACTIVE7;
@@ -532,7 +681,10 @@ Character *GuildLocation::doOptions(Character *c) {
if (_buttonValue < (int)party._activeParty.size()) {
c = &party._activeParty[_buttonValue];
intf.highlightChar(_buttonValue);
-
+#ifdef USE_TTS
+ _vm->stopTextToSpeech();
+ _resetText = true;
+#endif
if (!c->guildMember()) {
sound.stopSound();
_animFrame = 5;
@@ -554,6 +706,38 @@ Character *GuildLocation::doOptions(Character *c) {
return c;
}
+#ifdef USE_TTS
+
+void GuildLocation::speakTextAndButtons(const Common::String &text) {
+ uint index = 0;
+ bool isInGuild = g_vm->_party->_activeParty[_buttonValue].guildMember();
+
+ // Header
+ if (isInGuild) { // "Guild options for" and character name
+ _vm->sayText(getNextTextSection(text, index, 2, " "));
+ } else { // "Guild options for" and "need to be a member"
+ _vm->sayText(getNextTextSection(text, index, 2));
+ }
+
+ _buttonTexts.clear();
+
+ if (isInGuild) {
+ // Options
+ _vm->sayText(addNextTextToButtons(text, index, 2));
+ } else {
+ // Add empty buffers for the usual options strings, so the escape button aligns properly with the text
+ _buttonTexts.resize(2);
+ }
+
+ // Gold
+ _vm->sayText(getNextTextSection(text, index, 2, ": "));
+
+ // ESC
+ _vm->sayText(addNextTextToButtons(text, index));
+}
+
+#endif
+
/*------------------------------------------------------------------------*/
TavernLocation::TavernLocation() : BaseLocation(TAVERN) {
@@ -564,12 +748,12 @@ TavernLocation::TavernLocation() : BaseLocation(TAVERN) {
loadStrings("tavern.bin");
_icons1.load("tavern.icn");
- addButton(Common::Rect(281, 108, 305, 128), Common::KEYCODE_ESCAPE, &_icons1);
- addButton(Common::Rect(242, 108, 266, 128), Res.KeyConstants.Locations.KEY_SIGN_IN, &_icons1);
- addButton(Common::Rect(234, 54, 308, 62), Res.KeyConstants.Locations.KEY_DRINK);
- addButton(Common::Rect(234, 64, 308, 72), Res.KeyConstants.Locations.KEY_FOOD);
- addButton(Common::Rect(234, 74, 308, 82), Res.KeyConstants.Locations.KEY_TIP);
- addButton(Common::Rect(234, 84, 308, 92), Res.KeyConstants.Locations.KEY_RUMORS);
+ addButton(Common::Rect(281, 108, 305, 128), Common::KEYCODE_ESCAPE, &_icons1, kTavernExit);
+ addButton(Common::Rect(242, 108, 266, 128), Res.KeyConstants.Locations.KEY_SIGN_IN, &_icons1, kTavernSignIn);
+ addButton(Common::Rect(234, 54, 308, 62), Res.KeyConstants.Locations.KEY_DRINK, nullptr, kTavernDrink);
+ addButton(Common::Rect(234, 64, 308, 72), Res.KeyConstants.Locations.KEY_FOOD, nullptr, kTavernFood);
+ addButton(Common::Rect(234, 74, 308, 82), Res.KeyConstants.Locations.KEY_TIP, nullptr, kTavernTip);
+ addButton(Common::Rect(234, 84, 308, 92), Res.KeyConstants.Locations.KEY_RUMORS, nullptr, kTavernRumors);
g_vm->_mode = MODE_INTERACTIVE7;
_vocName = _ccNum ? "hello1.voc" : "hello.voc";
@@ -601,6 +785,9 @@ Character *TavernLocation::doOptions(Character *c) {
_buttonValue -= Common::KEYCODE_F1;
if (_buttonValue < (int)party._activeParty.size()) {
c = &party._activeParty[_buttonValue];
+#ifdef USE_TTS
+ _vm->sayText(c->_name, Common::TextToSpeechManager::INTERRUPT);
+#endif
intf.highlightChar(_buttonValue);
_v21 = 0;
}
@@ -612,9 +799,18 @@ Character *TavernLocation::doOptions(Character *c) {
sound.playSound("gulp.voc");
_v21 = 1;
+#ifdef USE_TTS
+ // Disable voicing for option buttons
+ disableButtonVoicing(2, _buttons.size());
+#endif
+
+ Common::String ttsMessage;
windows[10].writeString(Common::String::format(Res.TAVERN_TEXT,
c->_name.c_str(), Res.GOOD_STUFF,
- XeenEngine::printMil(party._gold).c_str()));
+ XeenEngine::printMil(party._gold).c_str()), false, &ttsMessage);
+#ifdef USE_TTS
+ speakNotificationText(ttsMessage);
+#endif
drawButtons(&windows[0]);
windows[10].update();
@@ -625,6 +821,10 @@ Character *TavernLocation::doOptions(Character *c) {
}
wait();
+#ifdef USE_TTS
+ _vm->stopTextToSpeech();
+ enableButtonVoicing(2, _buttons.size());
+#endif
}
}
} else if (Res.KeyConstants.Locations.KEY_FOOD == _buttonValue) {
@@ -663,7 +863,13 @@ Character *TavernLocation::doOptions(Character *c) {
if (YesNo::show(_vm, false, true)) {
if (party._food >= _v22) {
+#ifdef USE_TTS
+ disableButtonVoicing(0, _buttons.size());
+#endif
ErrorScroll::show(_vm, Res.FOOD_PACKS_FULL, WT_LOC_WAIT);
+#ifdef USE_TTS
+ enableButtonVoicing(0, _buttons.size());
+#endif
} else if (party.subtract(CONS_GOLD, _v23, WHERE_PARTY, WT_LOC_WAIT)) {
party._food = _v22;
sound.stopSound();
@@ -673,6 +879,9 @@ Character *TavernLocation::doOptions(Character *c) {
windows[12].close();
windows[10].open();
+#ifdef USE_TTS
+ _vm->stopTextToSpeech();
+#endif
_buttonValue = 0;
} else if (Res.KeyConstants.Locations.KEY_RUMORS == _buttonValue) {
// Rumors
@@ -684,6 +893,9 @@ Character *TavernLocation::doOptions(Character *c) {
idx = 20;
}
+#ifdef USE_TTS
+ disableButtonVoicing(0, _buttons.size());
+#endif
Common::String msg = Common::String::format("\x03""c\x0B""012%s",
_textStrings[(party._day % 10) + idx].c_str());
Window &w = windows[12];
@@ -692,6 +904,10 @@ Character *TavernLocation::doOptions(Character *c) {
w.update();
wait();
+#ifdef USE_TTS
+ enableButtonVoicing(0, _buttons.size());
+ _vm->stopTextToSpeech();
+#endif
w.close();
} else if (Res.KeyConstants.Locations.KEY_SIGN_IN == _buttonValue) {
// Sign In
@@ -760,21 +976,46 @@ Character *TavernLocation::doOptions(Character *c) {
} else if (Res.KeyConstants.Locations.KEY_TIP == _buttonValue) {
if (!c->noActions()) {
if (!_v21) {
+#ifdef USE_TTS
+ disableButtonVoicing(0, _buttons.size());
+#endif
+ Common::String ttsMessage;
windows[10].writeString(Common::String::format(Res.TAVERN_TEXT,
c->_name.c_str(), Res.HAVE_A_DRINK,
- XeenEngine::printMil(party._gold).c_str()));
+ XeenEngine::printMil(party._gold).c_str()), false, &ttsMessage);
drawButtons(&windows[0]);
windows[10].update();
+
+#ifdef USE_TTS
+ speakNotificationText(ttsMessage);
+#endif
+
wait();
+#ifdef USE_TTS
+ enableButtonVoicing(0, _buttons.size());
+#endif
} else {
_v21 = 0;
if (c->_conditions[DRUNK]) {
+#ifdef USE_TTS
+ disableButtonVoicing(2, _buttons.size());
+#endif
+ Common::String ttsMessage;
windows[10].writeString(Common::String::format(Res.TAVERN_TEXT,
c->_name.c_str(), Res.YOURE_DRUNK,
- XeenEngine::printMil(party._gold).c_str()));
+ XeenEngine::printMil(party._gold).c_str()), false, &ttsMessage);
drawButtons(&windows[0]);
windows[10].update();
+
+#ifdef USE_TTS
+ speakNotificationText(ttsMessage);
+#endif
+
wait();
+#ifdef USE_TTS
+ _vm->stopTextToSpeech();
+ enableButtonVoicing(2, _buttons.size());
+#endif
} else if (party.subtract(CONS_GOLD, 1, WHERE_PARTY, WT_LOC_WAIT)) {
sound.stopSound();
sound.playSound(_ccNum ? "thanks2.voc" : "thankyou.voc", 1);
@@ -791,6 +1032,9 @@ Character *TavernLocation::doOptions(Character *c) {
_v24 = 50;
}
+#ifdef USE_TTS
+ disableButtonVoicing(0, _buttons.size());
+#endif
Common::String msg = _textStrings[map.mazeData()._tavernTips + _v24];
map.mazeData()._tavernTips = (map.mazeData()._tavernTips + 1) /
(_ccNum ? 10 : 15);
@@ -800,6 +1044,10 @@ Character *TavernLocation::doOptions(Character *c) {
w.writeString(Common::String::format("\x03""c\x0B""012%s", msg.c_str()));
w.update();
wait();
+#ifdef USE_TTS
+ enableButtonVoicing(0, _buttons.size());
+ _vm->stopTextToSpeech();
+#endif
w.close();
}
}
@@ -822,6 +1070,33 @@ void TavernLocation::farewell() {
map.mazeData()._mazeNumber = party._mazeId;
}
+#ifdef USE_TTS
+
+void TavernLocation::speakTextAndButtons(const Common::String &text) {
+ uint index = 0;
+ // Header
+ _vm->sayText(getNextTextSection(text, index, 2, " "));
+
+ // Options
+ _vm->sayText(addNextTextToButtons(text, index, kTavernOptionsCount));
+
+ // Gold
+ _vm->sayText(getNextTextSection(text, index, 2, ": "));
+
+ // Sign in/ESC buttons
+ _vm->sayText(addNextTextToButtons(text, index, kTavernButtonCount));
+}
+
+void TavernLocation::speakNotificationText(const Common::String &text) const {
+ uint index = 0;
+ // Header
+ getNextTextSection(text, index, 2);
+ // Info
+ _vm->sayText(getNextTextSection(text, index, 2));
+}
+
+#endif
+
/*------------------------------------------------------------------------*/
TempleLocation::TempleLocation() : BaseLocation(TEMPLE) {
@@ -837,10 +1112,10 @@ TempleLocation::TempleLocation() : BaseLocation(TEMPLE) {
_v5 = _v6 = 0;
_icons1.load("esc.icn");
- addButton(Common::Rect(261, 108, 285, 128), Common::KEYCODE_ESCAPE, &_icons1);
- addButton(Common::Rect(234, 54, 308, 62), Res.KeyConstants.Locations.KEY_HEAL);
- addButton(Common::Rect(234, 64, 308, 72), Res.KeyConstants.Locations.KEY_DONATION);
- addButton(Common::Rect(234, 74, 308, 82), Res.KeyConstants.Locations.KEY_UNCURSE);
+ addButton(Common::Rect(261, 108, 285, 128), Common::KEYCODE_ESCAPE, &_icons1, kTempleExit);
+ addButton(Common::Rect(234, 54, 308, 62), Res.KeyConstants.Locations.KEY_HEAL, nullptr, kTempleHeal);
+ addButton(Common::Rect(234, 64, 308, 72), Res.KeyConstants.Locations.KEY_DONATION, nullptr, kTempleDonation);
+ addButton(Common::Rect(234, 74, 308, 82), Res.KeyConstants.Locations.KEY_UNCURSE, nullptr, kTempleUncurse);
addButton(Common::Rect(234, 84, 308, 92), 0);
_vocName = _ccNum ? "help2.voc" : "maywe2.voc";
@@ -924,6 +1199,9 @@ Character *TempleLocation::doOptions(Character *c) {
_buttonValue -= Common::KEYCODE_F1;
if (_buttonValue < (int)party._activeParty.size()) {
c = &party._activeParty[_buttonValue];
+#ifdef USE_TTS
+ _resetText = true;
+#endif
intf.highlightChar(_buttonValue);
_dayOfWeek = 0;
}
@@ -989,6 +1267,31 @@ Character *TempleLocation::doOptions(Character *c) {
return c;
}
+#ifdef USE_TTS
+
+void TempleLocation::speakTextAndButtons(const Common::String &text) {
+ uint index = 0;
+
+ // Header ("Options for" and character name)
+ _vm->sayText(getNextTextSection(text, index, 2, " "));
+
+ // Options
+ for (uint8 i = 0; i < kTempleOptionsCount; ++i) {
+ // Text and price
+ Common::String optionText = getNextTextSection(text, index, 2, ": ");
+ _buttonTexts.push_back(optionText);
+ _vm->sayText(optionText);
+ }
+
+ // Gold
+ _vm->sayText(getNextTextSection(text, index, 2, ": "));
+
+ // ESC button
+ _vm->sayText(addNextTextToButtons(text, index));
+}
+
+#endif
+
/*------------------------------------------------------------------------*/
TrainingLocation::TrainingLocation() : BaseLocation(TRAINING) {
@@ -997,8 +1300,8 @@ TrainingLocation::TrainingLocation() : BaseLocation(TRAINING) {
_charIndex = 0;
_icons1.load("train.icn");
- addButton(Common::Rect(281, 108, 305, 128), Common::KEYCODE_ESCAPE, &_icons1);
- addButton(Common::Rect(242, 108, 266, 128), Res.KeyConstants.Locations.KEY_TRAIN, &_icons1);
+ addButton(Common::Rect(281, 108, 305, 128), Common::KEYCODE_ESCAPE, &_icons1, kTrainingExit);
+ addButton(Common::Rect(242, 108, 266, 128), Res.KeyConstants.Locations.KEY_TRAIN, &_icons1, kTrainingTrain);
_vocName = _ccNum ? "youtrn1.voc" : "training.voc";
}
@@ -1077,6 +1380,10 @@ Character *TrainingLocation::doOptions(Character *c) {
if (_buttonValue < (int)party._activeParty.size()) {
_charIndex = _buttonValue;
c = &party._activeParty[_buttonValue];
+#ifdef USE_TTS
+ _vm->sayText(c->_name, Common::TextToSpeechManager::INTERRUPT);
+#endif
+ _ttsVoiceText = true;
intf.highlightChar(_buttonValue);
}
} else if (Res.KeyConstants.Locations.KEY_TRAIN == _buttonValue) {
@@ -1112,6 +1419,7 @@ Character *TrainingLocation::doOptions(Character *c) {
c->_currentHp = c->getMaxHP();
c->_currentSp = c->getMaxSP();
intf.drawParty(true);
+ _ttsVoiceText = true;
}
}
}
@@ -1119,6 +1427,34 @@ Character *TrainingLocation::doOptions(Character *c) {
return c;
}
+#ifdef USE_TTS
+
+void TrainingLocation::speakTextAndButtons(const Common::String &text) {
+ uint index = 0;
+ uint infoCount;
+
+ if (_experienceToNextLevel || g_vm->_party->_activeParty[_buttonValue]._level._permanent >= maxLevel()) {
+ infoCount = kTrainingIneligibleInfoCount;
+ } else {
+ infoCount = kTrainingEligibleInfoCount;
+ }
+
+ // Header and info
+ _vm->sayText(getNextTextSection(text, index, infoCount));
+
+ // Gold
+ _vm->sayText(getNextTextSection(text, index, 2, ": "));
+
+ // Train/ESC buttons
+ if (_buttonTexts.empty()) {
+ _vm->sayText(addNextTextToButtons(text, index, 2));
+ } else {
+ _vm->sayText(getNextTextSection(text, index, 2));
+ }
+}
+
+#endif
+
/*------------------------------------------------------------------------*/
ArenaLocation::ArenaLocation() : BaseLocation(ARENA) {
@@ -2381,12 +2717,21 @@ bool LocationMessage::execute(int portrait, const Common::String &name, const Co
int result = -1;
Common::String msgText = text;
+#ifdef USE_TTS
+ bool voiceName = true;
+#endif
do {
Common::String msg = Common::String::format(g_vm->getLanguage() == Common::ZH_TWN ? "\r\v014\x0c""07\x03""c\t125%s\x0c""04\x03""l\t000\v044%s" : "\r\v014\x03""c\t125%s\t000\v054%s",
name.c_str(), msgText.c_str());
+ Common::String ttsMessage;
// Count the number of words
- const char *msgEnd = w.writeString(msg);
+ const char *msgEnd = w.writeString(msg, false, &ttsMessage);
+#ifdef USE_TTS
+ speakText(ttsMessage, name, voiceName);
+ voiceName = false;
+#endif
+
int wordCount = 0;
for (const char *msgP = msg.c_str(); msgP != msgEnd && *msgP; ++msgP) {
@@ -2452,6 +2797,9 @@ bool LocationMessage::execute(int portrait, const Common::String &name, const Co
if (!confirm)
intf.mainIconsPrint();
+#ifdef USE_TTS
+ _vm->stopTextToSpeech();
+#endif
_townSprites[0].clear();
_townSprites[1].clear();
events.clearEvents();
@@ -2466,5 +2814,28 @@ void LocationMessage::loadButtons() {
addButton(Common::Rect(), Common::KEYCODE_ESCAPE);
}
+#ifdef USE_TTS
+
+void LocationMessage::speakText(const Common::String &text, const Common::String &name, bool voiceName) const {
+ uint index = 0;
+ _vm->stopTextToSpeech();
+
+ Common::String cleanedName;
+ // If the name has a newline character, it has two parts
+ if (name.find('\n') != Common::String::npos) {
+ cleanedName = getNextTextSection(text, index, 2, " ");
+ } else {
+ cleanedName = getNextTextSection(text, index);
+ }
+
+ if (voiceName) {
+ _vm->sayText(cleanedName);
+ }
+
+ _vm->sayText(text.substr(index));
+}
+
+#endif
+
} // End of namespace Xeen
} // End of namespace MM
diff --git a/engines/mm/xeen/locations.h b/engines/mm/xeen/locations.h
index 5ab0cd290af..c5970dc8e59 100644
--- a/engines/mm/xeen/locations.h
+++ b/engines/mm/xeen/locations.h
@@ -55,6 +55,10 @@ protected:
uint _farewellTime;
int _drawCtr1, _drawCtr2;
bool _exitToUi;
+ bool _ttsVoiceText;
+#ifdef USE_TTS
+ bool _resetText;
+#endif
protected:
/**
* Draw the window
@@ -80,6 +84,10 @@ protected:
return c;
}
+#ifdef USE_TTS
+ virtual void speakTextAndButtons(const Common::String &text) { }
+#endif
+
/**
* Handle any farewell
*/
@@ -111,6 +119,21 @@ private:
* Handles deposits or withdrawls fro the bank
*/
void depositWithdrawl(PartyBank whereId);
+
+#ifdef USE_TTS
+ /**
+ * Voices text with TTS and sets up button texts
+ * @param text All text, with each section separated by newlines
+ */
+ void speakTextAndButtons(const Common::String &text) override;
+
+ /**
+ * Voices text with TTS and sets up button texts for the deposit/withdrawal menu
+ * @param text All text, with each section separated by newlines
+ * @param oldButtonTexts Array to put old button texts in
+ */
+ void speakDepositWithdrawalText(const Common::String &text, Common::String oldButtonTexts[]);
+#endif
protected:
/**
* Generates the display text for the location, for a given character
@@ -133,6 +156,14 @@ public:
};
class BlacksmithLocation : public BaseLocation {
+private:
+#ifdef USE_TTS
+ /**
+ * Voices text with TTS and sets up button texts
+ * @param text Text for voicing and button texts. Each section should be separated by a newline
+ */
+ void speakTextAndButtons(const Common::String &text) override;
+#endif
protected:
/**
* Generates the display text for the location, for a given character
@@ -155,6 +186,14 @@ public:
};
class GuildLocation : public BaseLocation {
+private:
+#ifdef USE_TTS
+ /**
+ * Voices text with TTS and sets up button texts
+ * @param text Text for voicing and button texts. Each section should be separated by a newline
+ */
+ void speakTextAndButtons(const Common::String &text) override;
+#endif
protected:
/**
* Generates the display text for the location, for a given character
@@ -172,6 +211,20 @@ public:
};
class TavernLocation : public BaseLocation {
+private:
+#ifdef USE_TTS
+ /**
+ * Voices text with TTS and sets up button texts
+ * @param text Text for voicing and button texts. Each section should be separated by a newline
+ */
+ void speakTextAndButtons(const Common::String &text) override;
+
+ /**
+ * Voices "drunk" and similar notification texts with TTS
+ * @param text Text for voicing. Each section should be separated by a newline
+ */
+ void speakNotificationText(const Common::String &text) const;
+#endif
private:
int _v21;
uint _v22;
@@ -199,6 +252,14 @@ public:
};
class TempleLocation : public BaseLocation {
+private:
+#ifdef USE_TTS
+ /**
+ * Voices text with TTS and sets up button texts
+ * @param text Text for voicing and button texts. Each section should be separated by a newline
+ */
+ void speakTextAndButtons(const Common::String &text) override;
+#endif
private:
int _currentCharLevel;
int _donation;
@@ -226,6 +287,14 @@ public:
};
class TrainingLocation : public BaseLocation {
+private:
+#ifdef USE_TTS
+ /**
+ * Voices text with TTS and sets up button texts
+ * @param text Text for voicing and button texts. Each section should be separated by a newline
+ */
+ void speakTextAndButtons(const Common::String &text) override;
+#endif
private:
int _charIndex;
bool _charsTrained[MAX_ACTIVE_PARTY];
@@ -373,6 +442,10 @@ private:
const Common::String &text, int confirm);
void loadButtons();
+
+#ifdef USE_TTS
+ void speakText(const Common::String &text, const Common::String &name, bool voiceName) const;
+#endif
public:
static bool showMessage(int portrait, const Common::String &name,
const Common::String &text, int confirm);
diff --git a/engines/mm/xeen/party.cpp b/engines/mm/xeen/party.cpp
index bae9d802003..3edef1d2fbb 100644
--- a/engines/mm/xeen/party.cpp
+++ b/engines/mm/xeen/party.cpp
@@ -722,6 +722,9 @@ void Party::giveTreasure() {
events.clearEvents();
w.close();
w.open();
+#ifdef USE_TTS
+ _vm->stopTextToSpeech();
+#endif
w.writeString(Common::String::format(Res.PARTY_FOUND, _treasure._gold, _treasure._gems));
w.update();
@@ -801,6 +804,9 @@ void Party::giveTreasure() {
_gems += _treasure._gems;
_treasure._gold = 0;
_treasure._gems = 0;
+#ifdef USE_TTS
+ _vm->stopTextToSpeech();
+#endif
_treasure._hasItems = false;
_treasure.clear();
@@ -850,7 +856,12 @@ void Party::giveTreasureToCharacter(Character &c, ItemCategory category, int ite
if (index >= (_vm->getGameID() == GType_Swords ? 88 : 82)) {
// Quest item, give an extra '*' prefix
Common::String format = Common::String::format("\f04 * \fd%s", itemName);
- w.writeString(Common::String::format(Res.X_FOUND_Y, c._name.c_str(), getFoundForm(c), format.c_str()));
+ Common::String ttsMessage;
+ w.writeString(Common::String::format(Res.X_FOUND_Y, c._name.c_str(), getFoundForm(c), format.c_str()), false, &ttsMessage);
+#ifdef USE_TTS
+ ttsMessage.replace('*', ' ');
+ _vm->sayText(ttsMessage);
+#endif
} else {
w.writeString(Common::String::format(Res.X_FOUND_Y, c._name.c_str(), getFoundForm(c), itemName));
}
diff --git a/engines/mm/xeen/scripts.cpp b/engines/mm/xeen/scripts.cpp
index 81b154ea5b8..c940d9a737f 100644
--- a/engines/mm/xeen/scripts.cpp
+++ b/engines/mm/xeen/scripts.cpp
@@ -257,6 +257,9 @@ int Scripts::checkEvents() {
} else {
Window &w = windows[38];
w.open();
+#ifdef USE_TTS
+ _vm->stopTextToSpeech();
+#endif
w.writeString(Res.NOTHING_HERE);
w.update();
@@ -266,7 +269,9 @@ int Scripts::checkEvents() {
events.wait(1);
} while (!events.isKeyMousePressed() && !_vm->shouldExit());
events.clearEvents();
-
+#ifdef USE_TTS
+ _vm->stopTextToSpeech();
+#endif
w.close();
}
diff --git a/engines/mm/xeen/subtitles.cpp b/engines/mm/xeen/subtitles.cpp
index 91e80ab7fb1..c704a436e51 100644
--- a/engines/mm/xeen/subtitles.cpp
+++ b/engines/mm/xeen/subtitles.cpp
@@ -19,6 +19,7 @@
*
*/
+#include "common/config-manager.h"
#include "common/scummsys.h"
#include "mm/xeen/subtitles.h"
#include "mm/xeen/events.h"
@@ -90,6 +91,13 @@ void Subtitles::setLine(int line) {
_lineSize = _lines[_lineNum].size();
_lineEnd = 1;
_displayLine.clear();
+
+#ifdef USE_TTS
+ // Only voice subtitles if there's no voice
+ if ((ConfMan.hasKey("subtitles") && ConfMan.getBool("subtitles")) || ConfMan.getInt("speech_volume") == 0) {
+ g_vm->sayText(_lines[_lineNum], Common::TextToSpeechManager::QUEUE_NO_REPEAT);
+ }
+#endif
}
bool Subtitles::active() const {
@@ -134,7 +142,8 @@ void Subtitles::show() {
// Subtitles aren't needed
reset();
} else {
- if (timeElapsed()) {
+ Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();
+ if (timeElapsed() && (_lineEnd != _lineSize - 1 || !ttsMan || !ttsMan->isSpeaking())) {
_lineEnd = (_lineEnd + 1) % _lineSize;
int count;
if (Common::RU_RUS == g_vm->getLanguage())
@@ -159,9 +168,9 @@ void Subtitles::show() {
_boxSprites->draw(0, 0, Common::Point(36, 189));
// Write the subtitle line
- windows[0].writeString(_displayLine);
+ windows[0].writeString(_displayLine, false);
- if (_lineEnd == 0)
+ if (_lineEnd == 0 && (!ttsMan || !ttsMan->isSpeaking()))
reset();
}
}
diff --git a/engines/mm/xeen/swordsofxeen/swordsofxeen_menu.cpp b/engines/mm/xeen/swordsofxeen/swordsofxeen_menu.cpp
index b00442f960a..e8a2a0a87f2 100644
--- a/engines/mm/xeen/swordsofxeen/swordsofxeen_menu.cpp
+++ b/engines/mm/xeen/swordsofxeen/swordsofxeen_menu.cpp
@@ -28,6 +28,18 @@ namespace MM {
namespace Xeen {
namespace SwordsOfXeen {
+#ifdef USE_TTS
+
+static const char *swordsMainMenuButtons = "Start\nLoad\nView Credits";
+
+#endif
+
+enum SwordsMainMenuButtonTTSTextIndex {
+ kSwordsStart = 0,
+ kSwordsLoad = 1,
+ kSwordsCredits = 2
+};
+
void MainMenu::show(XeenEngine *vm) {
MainMenu *dlg = new MainMenu(vm);
dlg->execute();
@@ -48,6 +60,9 @@ void MainMenu::execute() {
events.setCursor(0);
events.showCursor();
sound.playSong("newbrigh.m");
+#ifdef USE_TTS
+ setButtonTexts(swordsMainMenuButtons);
+#endif
do {
// Draw the screen
@@ -99,9 +114,9 @@ void MainMenu::execute() {
}
void MainMenu::loadButtons() {
- addButton(Common::Rect(93, 87, 227, 97), Common::KEYCODE_s);
- addButton(Common::Rect(93, 98, 227, 108), Common::KEYCODE_l);
- addButton(Common::Rect(93, 110, 227, 120), Common::KEYCODE_v);
+ addButton(Common::Rect(93, 87, 227, 97), Common::KEYCODE_s, nullptr, kSwordsStart);
+ addButton(Common::Rect(93, 98, 227, 108), Common::KEYCODE_l, nullptr, kSwordsLoad);
+ addButton(Common::Rect(93, 110, 227, 120), Common::KEYCODE_v, nullptr, kSwordsCredits);
}
} // End of namespace SwordsOfXeen
diff --git a/engines/mm/xeen/window.h b/engines/mm/xeen/window.h
index 3e5a28ec408..dbb004e7d1f 100644
--- a/engines/mm/xeen/window.h
+++ b/engines/mm/xeen/window.h
@@ -147,13 +147,15 @@ public:
/**
* Write a string to the window
- * @param s String to display
- * @returns Any string remainder that couldn't be displayed
+ * @param s String to display
+ * @param ttsVoiceText Whether to voice the text with the TTS system
+ * @param ttsMessage Message for TTS, which will be built
+ * @returns Any string remainder that couldn't be displayed
* @remarks Note that bounds is just used for wrapping purposes. Unless
* justification is set, the message will be written at _writePos
*/
- const char *writeString(const Common::String &s) {
- return FontSurface::writeString(s, _innerBounds);
+ const char *writeString(const Common::String &s, bool ttsVoiceText = true, Common::String *ttsMessage = nullptr) {
+ return FontSurface::writeString(s, _innerBounds, ttsVoiceText, ttsMessage);
}
/**
diff --git a/engines/mm/xeen/worldofxeen/clouds_cutscenes.cpp b/engines/mm/xeen/worldofxeen/clouds_cutscenes.cpp
index e93ad8e4e2b..97918013b1e 100644
--- a/engines/mm/xeen/worldofxeen/clouds_cutscenes.cpp
+++ b/engines/mm/xeen/worldofxeen/clouds_cutscenes.cpp
@@ -259,6 +259,8 @@ bool CloudsCutscenes::showCloudsIntroInner() {
screen.update();
_subtitles.setLine(0);
+ Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();
+
// Loop through each spoken line
int ctr1 = 0, ctr2 = 0, ctr3 = 0, ctr4 = 0, ctr5 = 0, totalCtr = 0;
for (int lineCtr = 0; lineCtr < 14; ++lineCtr) {
@@ -337,7 +339,7 @@ bool CloudsCutscenes::showCloudsIntroInner() {
case 12:
case 13: {
crodo.draw(0, 0, Common::Point(0, -5));
- windows[0].writeString(Res.CLOUDS_INTRO1);
+ windows[0].writeString(Res.CLOUDS_INTRO1, false);
ctr5 = (ctr5 + 1) % 19;
@@ -350,7 +352,7 @@ bool CloudsCutscenes::showCloudsIntroInner() {
if (lookup > 30)
lookup = 30;
frameCtr = _INTRO_FRAMES_VALS[_INTRO_FRAMES_LOOKUP[lineCtr]][lookup];
- windows[0].writeString(Res.CLOUDS_INTRO1);
+ windows[0].writeString(Res.CLOUDS_INTRO1, false);
ctr5 = (ctr5 + 1) % 19;
break;
@@ -359,7 +361,7 @@ bool CloudsCutscenes::showCloudsIntroInner() {
uint expiry = _INTRO_FRAMES_WAIT[_INTRO_FRAMES_LOOKUP[lineCtr]][lookup];
do {
WAIT(1);
- } while (events.timeElapsed1() < expiry);
+ } while (events.timeElapsed1() < expiry || (!sound.isSoundPlaying() && ttsMan && ttsMan->isSpeaking()));
++lookup;
if (!sound._fxOn && lookup > 30)
diff --git a/engines/mm/xeen/worldofxeen/worldofxeen_menu.cpp b/engines/mm/xeen/worldofxeen/worldofxeen_menu.cpp
index 9bc30ba10c3..9f3b3c1b28a 100644
--- a/engines/mm/xeen/worldofxeen/worldofxeen_menu.cpp
+++ b/engines/mm/xeen/worldofxeen/worldofxeen_menu.cpp
@@ -30,6 +30,86 @@ namespace MM {
namespace Xeen {
namespace WorldOfXeen {
+#ifdef USE_TTS
+
+static const uint8 kOtherOptionsMaxButtonCount = 5;
+
+static const char *worldMainMenuButtons[] = {
+ "Start a New Game\nLoad a Saved Game\nCredits\nOther Options\n", // English
+ "Neues Spiel\nSpielstand laden\nCredits\nWeitere Optionen\n", // German
+ "Nouvelle Partie\nCharger une Partie\nCredits\nOptions\n", // French
+ "Partida nueva\nCargar una partida\nCr\44ditos\nOtras opciones\n" // Spanish
+ // No Russian or Chinese versions exist
+};
+
+static const char *worldOtherOptionsButtons[] = {
+ "View Darkside Intro\nView Clouds Intro\nView Darkside End\nView Clouds End\nView World End\n", // English
+ "Darkside-Intro\nClouds-Intro\nDarkside-Ende\nClouds-Ende\nWorld-Ende\n", // German
+ "Intro de Darkside\nIntro des Nuages\nFin de Darkside\nFin des Nuages\nFin de World\n", // French
+ "Intro de Darkside\nIntro de Clouds\nVer Final de Darkside\nVer Final de Clouds\nVer Final de World\n", // Spanish
+ "", // Russian (no version exists)
+ // Chinese
+ "\xb6\xc2\xb7\x74\xbb\xe2\xa5\x44\xa4\xb6\xd2\xd0\n" // 黿é 主ä»ç´¹
+ "\xb6\xb3\xa4\xb6\xd2\xd0\n" // é²ä»ç´¹
+ "\xb6\xc2\xb7\x74\xbb\xe2\xa5\x44\xa5\xbd\xa7\xc0\n" // 黿é 主æ«å°¾
+ "\xb6\xb3\xa5\xbd\xa7\xc0\n" // 鲿«å°¾
+ "\xa5\x40\xac\xc9\n" // ä¸çæ«å°¾
+};
+
+static const char *cloudsMainMenuButtons[] = {
+ "Start a New Game\nLoad a Saved Game\nCredits\nView Endgame\n", // English
+ "Neues Spiel\nSpielstand laden\nCredits\nEndspiel ansehen\n", // German
+ "Nouvelle Partie\nCharger une Partie\nCr\202dits\nVoir la fin", // French
+ "", // Spanish (no version exists)
+ // Russian
+ "\x8d\xa0\xe7\xa0\xe2\xec \xad\xae\xa2\xe3\xee \xa8\xa3\xe0\xe3\n" // ÐаÑаÑÑ Ð½Ð¾Ð²ÑÑ Ð¸Ð³ÑÑ
+ "\x87\xa0\xa3\xe0\xe3\xa7\xa8\xe2\xec \xa8\xa3\xe0\xe3\n" // ÐагÑÑзиÑÑ Ð¸Ð³ÑÑ
+ "\x91\xae\xa7\xa4\xa0\xe2\xa5\xab\xa8\n" // СоздаÑели
+ "\x94\xa8\xad\xa0\xab\xec\xad\xeb\xa9 \xe0\xae\xab\xa8\xaa\n", // ФиналÑнÑй Ñолик
+ // Chinese
+ "\xb6\x7d\xa9\x6c\xb7\x73\xb9\x43\xc0\xb8\n" // éå§æ°éæ²
+ "\xb8\xfc\xa4\x4a\xb6\x69\xab\xd7\xc0\xc9\n" // è¼å
¥é²åº¦æª
+ "\xb9\x43\xc0\xb8\xbb\x73\xa7\x40\xb8\x73\n" // éæ²è£½ä½ç¾¤
+ "\xac\x64\xac\xdd\xb4\xdd\xa7\xbd\n" // æ¥çæ®å±
+};
+
+static const char *darksideMainMenuButtons[] = {
+ "Start\nLoad\nView Credits\nOther Options\n", // English
+ "Start\nLaden\nCredits\nWeitere Optionen\n", // German
+ "Nouveau\nPartie\nCredits\nOptions\n", // French
+ "", // Spanish (no version exists)
+ "", // Russian (no version exists)
+ // Chinese
+ "\xb6\x7d\xa9\x6c\xb7\x73\xb9\x43\xc0\xb8\n" // éå§æ°éæ²
+ "\xb8\xfc\xa4\x4a\xc2\xc2\xc0\xc9\n" // è¼å
¥èæª
+ "\xf7\x54\xac\xdd\xb3\x5d\xad\x70\xb8\x73\n" // 覾çè¨è¨ç¾¤
+ "\xa8\xe4\xa5\x4c\xbf\xef\xb6\xb5\n" // å
¶ä»é¸é
+};
+
+#endif
+
+enum CloudsMainMenuButtonTTSTextIndex {
+ kCloudsNew = 0,
+ kCloudsLoad = 1,
+ kCloudsCredits = 2,
+ kCloudsEndgame = 3
+};
+
+enum DarksideWorldMainMenuButtonTTSTextIndex {
+ kDarksideWorldStart = 0,
+ kDarksideWorldLoad = 1,
+ kDarksideWorldCredits = 2,
+ kDarksideWorldOther = 3
+};
+
+enum OtherOptionsButtonTTSTextIndex {
+ kDarksideIntro = 0,
+ kCloudsIntro = 1,
+ kDarksideEnd = 2,
+ kCloudsEnd = 3,
+ kWorldEnd = 4
+};
+
void MainMenuContainer::show() {
MainMenuContainer *menu;
@@ -105,11 +185,20 @@ void MainMenuContainer::execute() {
screen.doScroll(true, false);
+ if (_dialog) {
+ _dialog->_ttsVoiceText = true;
+ }
+
while (!g_vm->shouldExit() && g_vm->_gameMode == GMODE_NONE) {
// Draw the menu
draw();
- if (_dialog)
+ if (_dialog) {
_dialog->draw();
+#ifdef USE_TTS
+ _dialog->checkHoverOverButton();
+#endif
+ _dialog->_ttsVoiceText = false;
+ }
// Fade/scroll in screen if first frame showing screen
if (!showFlag) {
@@ -306,6 +395,9 @@ CloudsMenuDialog::CloudsMenuDialog(MainMenuContainer *owner) : MainMenuDialog(ow
w.open();
loadButtons();
+#ifdef USE_TTS
+ setButtonTexts(cloudsMainMenuButtons[_vm->_ttsLanguage]);
+#endif
}
CloudsMenuDialog::~CloudsMenuDialog() {
@@ -316,11 +408,11 @@ CloudsMenuDialog::~CloudsMenuDialog() {
void CloudsMenuDialog::loadButtons() {
_buttonSprites.load("start.icn");
- addButton(Common::Rect(93, 53, 227, 73), Res.KeyConstants.CloudsOfXeenMenu.KEY_START_NEW_GAME, &_buttonSprites);
- addButton(Common::Rect(93, 78, 227, 98), Res.KeyConstants.CloudsOfXeenMenu.KEY_LOAD_GAME, &_buttonSprites);
- addButton(Common::Rect(93, 103, 227, 123), Res.KeyConstants.CloudsOfXeenMenu.KEY_SHOW_CREDITS, &_buttonSprites);
+ addButton(Common::Rect(93, 53, 227, 73), Res.KeyConstants.CloudsOfXeenMenu.KEY_START_NEW_GAME, &_buttonSprites, kCloudsNew);
+ addButton(Common::Rect(93, 78, 227, 98), Res.KeyConstants.CloudsOfXeenMenu.KEY_LOAD_GAME, &_buttonSprites, kCloudsLoad);
+ addButton(Common::Rect(93, 103, 227, 123), Res.KeyConstants.CloudsOfXeenMenu.KEY_SHOW_CREDITS, &_buttonSprites, kCloudsCredits);
if (g_vm->_gameWon[0])
- addButton(Common::Rect(93, 128, 227, 148), Res.KeyConstants.CloudsOfXeenMenu.KEY_VIEW_ENDGAME, &_buttonSprites);
+ addButton(Common::Rect(93, 128, 227, 148), Res.KeyConstants.CloudsOfXeenMenu.KEY_VIEW_ENDGAME, &_buttonSprites, kCloudsEndgame);
}
void CloudsMenuDialog::draw() {
@@ -328,7 +420,7 @@ void CloudsMenuDialog::draw() {
Window &w = windows[GAME_WINDOW];
w.frame();
- w.writeString(Common::String::format(Res.OPTIONS_MENU, Res.GAME_NAMES[0], g_vm->_gameWon[0] ? 117 : 92, 1992));
+ w.writeString(Common::String::format(Res.OPTIONS_MENU, Res.GAME_NAMES[0], g_vm->_gameWon[0] ? 117 : 92, 1992), _ttsVoiceText);
drawButtons(&w);
}
@@ -360,6 +452,9 @@ DarkSideMenuDialog::DarkSideMenuDialog(MainMenuContainer *owner) : MainMenuDialo
w.open();
loadButtons();
+#ifdef USE_TTS
+ setButtonTexts(darksideMainMenuButtons[_vm->_ttsLanguage]);
+#endif
}
DarkSideMenuDialog::~DarkSideMenuDialog() {
@@ -369,10 +464,10 @@ DarkSideMenuDialog::~DarkSideMenuDialog() {
}
void DarkSideMenuDialog::loadButtons() {
- addButton(Common::Rect(124, 87, 177, 97), Common::KEYCODE_s);
- addButton(Common::Rect(126, 98, 173, 108), Common::KEYCODE_l);
- addButton(Common::Rect(91, 110, 209, 120), Common::KEYCODE_c);
- addButton(Common::Rect(85, 121, 216, 131), Common::KEYCODE_o);
+ addButton(Common::Rect(124, 87, 177, 97), Common::KEYCODE_s, nullptr, kDarksideWorldStart);
+ addButton(Common::Rect(126, 98, 173, 108), Common::KEYCODE_l, nullptr, kDarksideWorldLoad);
+ addButton(Common::Rect(91, 110, 209, 120), Common::KEYCODE_c, nullptr, kDarksideWorldCredits);
+ addButton(Common::Rect(85, 121, 216, 131), Common::KEYCODE_o, nullptr, kDarksideWorldOther);
}
void DarkSideMenuDialog::draw() {
@@ -450,6 +545,9 @@ WorldMenuDialog::WorldMenuDialog(MainMenuContainer *owner) : MainMenuDialog(owne
w.open();
loadButtons();
+#ifdef USE_TTS
+ setButtonTexts(worldMainMenuButtons[_vm->_ttsLanguage]);
+#endif
}
WorldMenuDialog::~WorldMenuDialog() {
@@ -460,10 +558,10 @@ WorldMenuDialog::~WorldMenuDialog() {
void WorldMenuDialog::loadButtons() {
_buttonSprites.load("start.icn");
- addButton(Common::Rect(93, 53, 227, 73), Common::KEYCODE_s, &_buttonSprites);
- addButton(Common::Rect(93, 78, 227, 98), Common::KEYCODE_l, &_buttonSprites);
- addButton(Common::Rect(93, 103, 227, 123), Common::KEYCODE_c, &_buttonSprites);
- addButton(Common::Rect(93, 128, 227, 148), Common::KEYCODE_o, &_buttonSprites);
+ addButton(Common::Rect(93, 53, 227, 73), Common::KEYCODE_s, &_buttonSprites, kDarksideWorldStart);
+ addButton(Common::Rect(93, 78, 227, 98), Common::KEYCODE_l, &_buttonSprites, kDarksideWorldLoad);
+ addButton(Common::Rect(93, 103, 227, 123), Common::KEYCODE_c, &_buttonSprites, kDarksideWorldCredits);
+ addButton(Common::Rect(93, 128, 227, 148), Common::KEYCODE_o, &_buttonSprites, kDarksideWorldOther);
}
void WorldMenuDialog::draw() {
@@ -471,7 +569,7 @@ void WorldMenuDialog::draw() {
Window &w = windows[GAME_WINDOW];
w.frame();
- w.writeString(Common::String::format(Res.OPTIONS_MENU, Res.GAME_NAMES[2], 117, 1993));
+ w.writeString(Common::String::format(Res.OPTIONS_MENU, Res.GAME_NAMES[2], 117, 1993), _ttsVoiceText);
drawButtons(&w);
}
@@ -527,39 +625,44 @@ void OtherOptionsDialog::loadButtons() {
Common::Rect r(93, 53, 227, 73);
// View Darkside Intro
- addButton(r, Common::KEYCODE_d, &_buttonSprites);
+ addButton(r, Common::KEYCODE_d, &_buttonSprites, kDarksideIntro);
r.translate(0, 25);
// View Clouds Intro
if (g_vm->getGameID() == GType_WorldOfXeen) {
- addButton(r, Common::KEYCODE_c, &_buttonSprites);
+ addButton(r, Common::KEYCODE_c, &_buttonSprites, kCloudsIntro);
r.translate(0, 25);
} else {
- addButton(Common::Rect(), Common::KEYCODE_INVALID);
+ addButton(Common::Rect(), Common::KEYCODE_INVALID, nullptr, kCloudsIntro);
}
// View Darkside End
if (g_vm->_gameWon[1]) {
- addButton(r, Common::KEYCODE_e, &_buttonSprites);
+ addButton(r, Common::KEYCODE_e, &_buttonSprites, kDarksideEnd);
r.translate(0, 25);
} else {
- addButton(Common::Rect(), Common::KEYCODE_INVALID);
+ addButton(Common::Rect(), Common::KEYCODE_INVALID, nullptr, kDarksideEnd);
}
// View Clouds End
if (g_vm->_gameWon[0]) {
- addButton(r, Common::KEYCODE_v, &_buttonSprites);
+ addButton(r, Common::KEYCODE_v, &_buttonSprites, kCloudsEnd);
r.translate(0, 25);
} else {
- addButton(Common::Rect(), Common::KEYCODE_INVALID);
+ addButton(Common::Rect(), Common::KEYCODE_INVALID, nullptr, kCloudsEnd);
}
// View World End
if (g_vm->_gameWon[2]) {
- addButton(r, Common::KEYCODE_w, &_buttonSprites);
+ addButton(r, Common::KEYCODE_w, &_buttonSprites, kWorldEnd);
} else {
- addButton(Common::Rect(), Common::KEYCODE_INVALID);
+ addButton(Common::Rect(), Common::KEYCODE_INVALID, nullptr, kWorldEnd);
}
+
+#ifdef USE_TTS
+ uint index = 0;
+ addNextTextToButtons(worldOtherOptionsButtons[_vm->_ttsLanguage], index, kOtherOptionsMaxButtonCount);
+#endif
}
void OtherOptionsDialog::draw() {
@@ -569,7 +672,7 @@ void OtherOptionsDialog::draw() {
w.frame();
w.writeString(Common::String::format(Res.OPTIONS_MENU,
Res.GAME_NAMES[g_vm->getGameID() == GType_WorldOfXeen ? 2 : 1],
- w.getBounds().height() - 33, 1993));
+ w.getBounds().height() - 33, 1993), _ttsVoiceText);
drawButtons(&w);
}
diff --git a/engines/mm/xeen/worldofxeen/worldofxeen_menu.h b/engines/mm/xeen/worldofxeen/worldofxeen_menu.h
index 2764a008bc8..b14bbf8a9b3 100644
--- a/engines/mm/xeen/worldofxeen/worldofxeen_menu.h
+++ b/engines/mm/xeen/worldofxeen/worldofxeen_menu.h
@@ -147,11 +147,13 @@ public:
class MenuContainerDialog : public ButtonContainer {
protected:
MainMenuContainer *_owner;
+public:
+ bool _ttsVoiceText;
public:
/**
* Constructor
*/
- MenuContainerDialog(MainMenuContainer *owner) : ButtonContainer(g_vm), _owner(owner) {}
+ MenuContainerDialog(MainMenuContainer *owner) : ButtonContainer(g_vm), _owner(owner), _ttsVoiceText(true) {}
/**
* Destructor
diff --git a/engines/mm/xeen/xeen.cpp b/engines/mm/xeen/xeen.cpp
index 17aefa70438..fceb3e18053 100644
--- a/engines/mm/xeen/xeen.cpp
+++ b/engines/mm/xeen/xeen.cpp
@@ -31,6 +31,16 @@
namespace MM {
namespace Xeen {
+#ifdef USE_TTS
+
+static const uint16 spanishEncodingTable[] = {
+ 0x23, 0xc3a1, 0x24, 0xc3a9, 0x25, 0xc3bc, 0x26, 0xc3b3, 0x3d, 0xc3b1, 0x5b, 0xc2bf, // á, é, ü, ó, ñ, ¿
+ 0x5c, 0xc39a, 0x5d, 0xc3ba, 0x5e, 0xc389, 0x5f, 0xc381, 0x7b, 0xc2a1, 0x7d, 0xc3ad, // Ã, ú, Ã, Ã, ¡, Ã
+ 0
+};
+
+#endif
+
XeenEngine *g_vm = nullptr;
XeenEngine::XeenEngine(OSystem *syst, const MightAndMagicGameDescription *gameDesc)
@@ -59,6 +69,9 @@ XeenEngine::XeenEngine(OSystem *syst, const MightAndMagicGameDescription *gameDe
_loadSaveSlot = -1;
_gameWon[0] = _gameWon[1] = _gameWon[2] = false;
_finalScore = 0;
+#ifdef USE_TTS
+ _mouseMoved = false;
+#endif
g_vm = this;
}
@@ -111,6 +124,46 @@ bool XeenEngine::initialize() {
// Setup mixer
syncSoundSettings();
+#ifdef USE_TTS
+ Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();
+ if (ttsMan != nullptr) {
+ ttsMan->enable(ConfMan.getBool("tts_enabled"));
+ ttsMan->setLanguage(ConfMan.get("language"));
+ }
+
+ switch (getLanguage()) {
+ case Common::EN_ANY:
+ _ttsLanguage = kEnglish;
+ break;
+ case Common::DE_DEU:
+ _ttsLanguage = kGerman;
+ break;
+ case Common::FR_FRA:
+ _ttsLanguage = kFrench;
+ break;
+ case Common::ES_ESP:
+ _ttsLanguage = kSpanish;
+ break;
+ case Common::RU_RUS:
+ _ttsLanguage = kRussian;
+ break;
+ case Common::ZH_TWN:
+ _ttsLanguage = kChinese;
+ break;
+ default:
+ _ttsLanguage = kEnglish;
+ break;
+ }
+
+ if (_ttsLanguage == kRussian) {
+ _ttsTextEncoding = Common::CodePage::kDos866;
+ } else if (_ttsLanguage == kChinese) {
+ _ttsTextEncoding = Common::CodePage::kBig5;
+ } else {
+ _ttsTextEncoding = Common::CodePage::kDos850;
+ }
+#endif
+
// Load settings
loadSettings();
@@ -327,6 +380,69 @@ void XeenEngine::GUIError(const Common::U32String &msg) {
GUIErrorMessage(msg);
}
+#ifdef USE_TTS
+
+void XeenEngine::sayText(const Common::String &text, Common::TextToSpeechManager::Action action) const {
+ if (text.empty()) {
+ return;
+ }
+
+ Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();
+ if (ttsMan && ConfMan.getBool("tts_enabled")) {
+ if (getLanguage() == Common::ES_ESP) {
+ ttsMan->say(convertSpanishText(text), action);
+ } else {
+ ttsMan->say(text, action, _ttsTextEncoding);
+ }
+ }
+}
+
+void XeenEngine::stopTextToSpeech() const {
+ Common::TextToSpeechManager *ttsMan = g_system->getTextToSpeechManager();
+ if (ttsMan && ConfMan.getBool("tts_enabled") && ttsMan->isSpeaking()) {
+ ttsMan->stop();
+ }
+}
+
+Common::U32String XeenEngine::convertSpanishText(const Common::String &text) const {
+ const byte *bytes = (const byte *)text.c_str();
+ byte *convertedBytes = new byte[text.size() * 2 + 1];
+
+ int i = 0;
+ for (const byte *b = bytes; *b; ++b) {
+ if (*b == 0x60) { // ` to =
+ convertedBytes[i] = 0x3d;
+ i++;
+ continue;
+ }
+
+ bool inTable = false;
+ for (uint j = 0; spanishEncodingTable[j]; j += 2) {
+ if (*b == spanishEncodingTable[j]) {
+ convertedBytes[i] = (spanishEncodingTable[j + 1] >> 8) & 0xff;
+ convertedBytes[i + 1] = spanishEncodingTable[j + 1] & 0xff;
+ i += 2;
+ inTable = true;
+ break;
+ }
+ }
+
+ if (!inTable) {
+ convertedBytes[i] = *b;
+ i++;
+ }
+ }
+
+ convertedBytes[i] = 0;
+
+ Common::U32String result((char *)convertedBytes);
+ delete[] convertedBytes;
+
+ return result;
+}
+
+#endif
+
uint32 XeenEngine::getSpecificGameId() const {
uint gameId = g_vm->getGameID();
diff --git a/engines/mm/xeen/xeen.h b/engines/mm/xeen/xeen.h
index aede957108c..fcb0fa04dbc 100644
--- a/engines/mm/xeen/xeen.h
+++ b/engines/mm/xeen/xeen.h
@@ -24,6 +24,7 @@
#include "common/scummsys.h"
#include "common/system.h"
+#include "common/text-to-speech.h"
#include "common/error.h"
#include "common/random.h"
#include "common/serializer.h"
@@ -88,6 +89,19 @@ enum GameMode {
GMODE_QUIT = 4
};
+#ifdef USE_TTS
+
+enum TTSLanguage {
+ kEnglish = 0,
+ kGerman = 1,
+ kFrench = 2,
+ kSpanish = 3,
+ kRussian = 4,
+ kChinese = 5
+};
+
+#endif
+
#define XEEN_SAVEGAME_VERSION 2
class XeenEngine : public MMEngine {
@@ -181,6 +195,11 @@ public:
bool _gameWon[3];
uint _finalScore;
ExtendedOptions _extOptions;
+#ifdef USE_TTS
+ bool _mouseMoved;
+ TTSLanguage _ttsLanguage;
+ Common::CodePage _ttsTextEncoding;
+#endif
CCArchive *_xeenCc = nullptr, *_darkCc = nullptr,
*_introCc = nullptr;
@@ -285,6 +304,25 @@ public:
* Show an error message in a GUI dialog
*/
void GUIError(const Common::U32String &msg);
+
+#ifdef USE_TTS
+ /**
+ * Voice text with the text-to-speech system
+ */
+ void sayText(const Common::String &text, Common::TextToSpeechManager::Action action = Common::TextToSpeechManager::QUEUE_NO_REPEAT) const;
+
+ /**
+ * Stop the text-to-speech system's speech
+ */
+ void stopTextToSpeech() const;
+
+ /**
+ * Convert text to a custom encoding for Spanish text
+ * @param text Text to convert
+ * @returns Converted string
+ */
+ Common::U32String convertSpanishText(const Common::String &text) const;
+#endif
};
extern XeenEngine *g_vm;
More information about the Scummvm-git-logs
mailing list