scummvm/engines/twine/menu/menu.cpp
2020-12-23 15:50:48 +01:00

1209 lines
41 KiB
C++

/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
*/
#include "twine/menu/menu.h"
#include "audio/mixer.h"
#include "backends/audiocd/audiocd.h"
#include "backends/keymapper/keymapper.h"
#include "common/config-manager.h"
#include "common/events.h"
#include "common/keyboard.h"
#include "common/scummsys.h"
#include "common/system.h"
#include "common/util.h"
#include "graphics/cursorman.h"
#include "twine/scene/actor.h"
#include "twine/scene/animations.h"
#include "twine/audio/music.h"
#include "twine/audio/sound.h"
#include "twine/scene/gamestate.h"
#include "twine/scene/grid.h"
#include "twine/resources/hqr.h"
#include "twine/input.h"
#include "twine/menu/interface.h"
#include "twine/menu/menuoptions.h"
#include "twine/scene/movements.h"
#include "twine/renderer/redraw.h"
#include "twine/renderer/renderer.h"
#include "twine/renderer/screens.h"
#include "twine/resources/resources.h"
#include "twine/scene/scene.h"
#include "twine/text.h"
#include "twine/twine.h"
namespace TwinE {
static const uint32 kPlasmaEffectFilesize = 262176;
namespace MenuButtonTypes {
enum _MenuButtonTypes {
kMusicVolume = 1,
kSoundVolume = 2,
kCDVolume = 3,
kLineVolume = 4,
kMasterVolume = 5,
kAgressiveMode = 6,
kPolygonDetails = 7,
kShadowSettings = 8,
kSceneryZoom = 9
};
}
#define checkMenuQuit(callMenu) \
if ((callMenu) == kQuitEngine) { \
return kQuitEngine; \
}
#define kBackground 9999
namespace _priv {
static MenuSettings createMainMenu() {
MenuSettings settings;
settings.setButtonsBoxHeight(200);
settings.addButton(TextId::kNewGame);
settings.addButton(TextId::kContinueGame);
settings.addButton(TextId::kOptions);
settings.addButton(TextId::kQuit);
return settings;
}
static MenuSettings createGiveUpMenu() {
MenuSettings settings;
settings.setButtonsBoxHeight(240);
settings.addButton(TextId::kContinue);
settings.addButton(TextId::kGiveUp);
return settings;
}
static MenuSettings createGiveUpSaveMenu() {
MenuSettings settings;
settings.setButtonsBoxHeight(240);
settings.addButton(TextId::kContinue);
settings.addButton(TextId::kCreateSaveGame);
settings.addButton(TextId::kGiveUp);
return settings;
}
static MenuSettings createOptionsMenu() {
MenuSettings settings;
settings.addButton(TextId::kReturnMenu);
settings.addButton(TextId::kVolumeSettings);
settings.addButton(TextId::kSaveManage);
settings.addButton(TextId::kAdvanced);
return settings;
}
static MenuSettings createAdvancedOptionsMenu() {
MenuSettings settings;
settings.addButton(TextId::kReturnMenu);
settings.addButton(TextId::kBehaviourAgressiveManual, MenuButtonTypes::kAgressiveMode);
settings.addButton(TextId::kDetailsPolygonsHigh, MenuButtonTypes::kPolygonDetails);
settings.addButton(TextId::kDetailsShadowHigh, MenuButtonTypes::kShadowSettings);
settings.addButton(TextId::kScenaryZoomOn, MenuButtonTypes::kSceneryZoom);
return settings;
}
static MenuSettings createSaveManageMenu() {
MenuSettings settings;
settings.addButton(TextId::kReturnMenu);
settings.addButton(TextId::kCreateSaveGame);
settings.addButton(TextId::kDeleteSaveGame);
return settings;
}
static MenuSettings createVolumeMenu() {
MenuSettings settings;
settings.addButton(TextId::kReturnMenu);
settings.addButton(TextId::kMusicVolume, MenuButtonTypes::kMusicVolume);
settings.addButton(TextId::kSoundVolume, MenuButtonTypes::kSoundVolume);
settings.addButton(TextId::kCDVolume, MenuButtonTypes::kCDVolume);
settings.addButton(TextId::kLineInVolume, MenuButtonTypes::kLineVolume);
settings.addButton(TextId::kMasterVolume, MenuButtonTypes::kMasterVolume);
settings.addButton(TextId::kSaveSettings);
return settings;
}
} // namespace _priv
const char *MenuSettings::getButtonText(Text *text, int buttonIndex) {
if (_buttonTexts[buttonIndex].empty()) {
const int32 textId = getButtonTextId(buttonIndex);
char dialText[256] = "";
text->getMenuText(textId, dialText, sizeof(dialText));
_buttonTexts[buttonIndex] = dialText;
}
return _buttonTexts[buttonIndex].c_str();
}
Menu::Menu(TwinEEngine *engine) {
_engine = engine;
optionsMenuState = _priv::createOptionsMenu();
giveUpMenuWithSaveState = _priv::createGiveUpSaveMenu();
volumeMenuState = _priv::createVolumeMenu();
saveManageMenuState = _priv::createSaveManageMenu();
giveUpMenuState = _priv::createGiveUpMenu();
mainMenuState = _priv::createMainMenu();
advOptionsMenuState = _priv::createAdvancedOptionsMenu();
Common::fill(&behaviourAnimState[0], &behaviourAnimState[4], 0);
Common::fill(&itemAngle[0], &itemAngle[255], 0);
}
Menu::~Menu() {
free(plasmaEffectPtr);
}
void Menu::plasmaEffectRenderFrame() {
for (int32 j = 1; j < PLASMA_HEIGHT - 1; j++) {
for (int32 i = 1; i < PLASMA_WIDTH - 1; i++) {
/* Here we calculate the average of all 8 neighbour pixel values */
int16 c;
c = plasmaEffectPtr[(i - 1) + (j - 1) * PLASMA_WIDTH]; //top-left
c += plasmaEffectPtr[(i + 0) + (j - 1) * PLASMA_WIDTH]; //top
c += plasmaEffectPtr[(i + 1) + (j - 1) * PLASMA_WIDTH]; //top-right
c += plasmaEffectPtr[(i - 1) + (j + 0) * PLASMA_WIDTH]; //left
c += plasmaEffectPtr[(i + 1) + (j + 0) * PLASMA_WIDTH]; //right
c += plasmaEffectPtr[(i - 1) + (j + 1) * PLASMA_WIDTH]; // bottom-left
c += plasmaEffectPtr[(i + 0) + (j + 1) * PLASMA_WIDTH]; // bottom
c += plasmaEffectPtr[(i + 1) + (j + 1) * PLASMA_WIDTH]; // bottom-right
/* And the 2 least significant bits are used as a
* randomizing parameter for statistically fading the flames */
c = (c >> 3) | ((c & 0x0003) << 13);
if (!(c & 0x6500) &&
(j >= (PLASMA_HEIGHT - 4) || c > 0)) {
c--; /*fade this pixel*/
}
/* plot the pixel using the calculated color */
plasmaEffectPtr[i + (PLASMA_HEIGHT + j) * PLASMA_WIDTH] = (uint8)c;
}
}
// flip the double-buffer while scrolling the effect vertically:
const uint8 *src = plasmaEffectPtr + (PLASMA_HEIGHT + 1) * PLASMA_WIDTH;
memcpy(plasmaEffectPtr, src, PLASMA_HEIGHT * PLASMA_WIDTH);
}
void Menu::processPlasmaEffect(int32 left, int32 top, int32 color) {
const int32 max_value = color + 15;
plasmaEffectRenderFrame();
const uint8 *in = plasmaEffectPtr + 5 * PLASMA_WIDTH;
uint8 *out = (uint8 *)_engine->frontVideoBuffer.getBasePtr(left, top);
for (int32 y = 0; y < PLASMA_HEIGHT / 2; y++) {
int32 yOffset = y * SCREEN_WIDTH;
const uint8 *colPtr = &in[y * PLASMA_WIDTH];
for (int32 x = 0; x < PLASMA_WIDTH; x++) {
const uint8 c = MIN(*colPtr / 2 + color, max_value);
/* 2x2 squares sharing the same pixel color: */
const int32 target = 2 * yOffset;
out[target + 0] = c;
out[target + 1] = c;
out[target + SCREEN_WIDTH + 0] = c;
out[target + SCREEN_WIDTH + 1] = c;
++colPtr;
++yOffset;
}
}
}
void Menu::drawBox(const Common::Rect &rect) {
_engine->_interface->drawLine(rect.left, rect.top, rect.right, rect.top, 79); // top line
_engine->_interface->drawLine(rect.left, rect.top, rect.left, rect.bottom, 79); // left line
_engine->_interface->drawLine(rect.right, rect.top + 1, rect.right, rect.bottom, 73); // right line
_engine->_interface->drawLine(rect.left + 1, rect.bottom, rect.right, rect.bottom, 73); // bottom line
}
void Menu::drawBox(int32 left, int32 top, int32 right, int32 bottom) {
drawBox(Common::Rect(left, top, right, bottom));
}
void Menu::drawButtonGfx(const MenuSettings *menuSettings, const Common::Rect &rect, int32 buttonId, const char *dialText, bool hover) {
if (hover) {
if (menuSettings == &volumeMenuState && buttonId <= MenuButtonTypes::kMasterVolume && buttonId >= MenuButtonTypes::kMusicVolume) {
int32 newWidth = 0;
switch (buttonId) {
case MenuButtonTypes::kMusicVolume: {
const int volume = _engine->_system->getMixer()->getVolumeForSoundType(Audio::Mixer::kMusicSoundType);
newWidth = _engine->_screens->crossDot(rect.left, rect.right, Audio::Mixer::kMaxMixerVolume, volume);
break;
}
case MenuButtonTypes::kSoundVolume: {
const int volume = _engine->_system->getMixer()->getVolumeForSoundType(Audio::Mixer::kSFXSoundType);
newWidth = _engine->_screens->crossDot(rect.left, rect.right, Audio::Mixer::kMaxMixerVolume, volume);
break;
}
case MenuButtonTypes::kCDVolume: {
const AudioCDManager::Status status = _engine->_system->getAudioCDManager()->getStatus();
newWidth = _engine->_screens->crossDot(rect.left, rect.right, Audio::Mixer::kMaxMixerVolume, status.volume);
break;
}
case MenuButtonTypes::kLineVolume: {
const int volume = _engine->_system->getMixer()->getVolumeForSoundType(Audio::Mixer::kSpeechSoundType);
newWidth = _engine->_screens->crossDot(rect.left, rect.right, Audio::Mixer::kMaxMixerVolume, volume);
break;
}
case MenuButtonTypes::kMasterVolume: {
const int volume = _engine->_system->getMixer()->getVolumeForSoundType(Audio::Mixer::kPlainSoundType);
newWidth = _engine->_screens->crossDot(rect.left, rect.right, Audio::Mixer::kMaxMixerVolume, volume);
break;
}
}
processPlasmaEffect(rect.left, rect.top, 80);
if (!(_engine->getRandomNumber() % 5)) {
plasmaEffectPtr[_engine->getRandomNumber() % 140 * 10 + 1900] = 255;
}
_engine->_interface->drawSplittedBox(Common::Rect(newWidth, rect.top, rect.right, rect.bottom), 68);
} else {
processPlasmaEffect(rect.left, rect.top, 64);
if (!(_engine->getRandomNumber() % 5)) {
plasmaEffectPtr[_engine->getRandomNumber() % 320 * 10 + 6400] = 255;
}
}
} else {
_engine->_interface->blitBox(rect, _engine->workVideoBuffer, _engine->frontVideoBuffer);
_engine->_interface->drawTransparentBox(rect, 4);
}
drawBox(rect);
_engine->_text->setFontColor(15);
_engine->_text->setFontParameters(2, 8);
const int32 textSize = _engine->_text->getTextSize(dialText);
_engine->_text->drawText((SCREEN_WIDTH / 2) - (textSize / 2), rect.top + 7, dialText);
_engine->copyBlockPhys(rect);
}
int16 Menu::drawButtons(MenuSettings *menuSettings, bool hover) {
int16 buttonNumber = menuSettings->getActiveButton();
const int32 maxButton = menuSettings->getButtonCount();
int32 topHeight = menuSettings->getButtonBoxHeight();
if (topHeight == 0) {
topHeight = 35;
} else {
topHeight = topHeight - (((maxButton - 1) * 6) + (maxButton * 50)) / 2;
}
if (maxButton <= 0) {
return -1;
}
int16 mouseActiveButton = -1;
for (int16 i = 0; i < maxButton; ++i) {
if (menuSettings == &advOptionsMenuState) {
int16 id = menuSettings->getButtonState(i);
switch (id) {
case MenuButtonTypes::kAgressiveMode:
if (_engine->_actor->autoAgressive) {
menuSettings->setButtonTextId(i, TextId::kBehaviourAgressiveAuto);
} else {
menuSettings->setButtonTextId(i, TextId::kBehaviourAgressiveManual);
}
break;
case MenuButtonTypes::kPolygonDetails:
if (_engine->cfgfile.PolygonDetails == 0) {
menuSettings->setButtonTextId(i, TextId::kDetailsPolygonsLow);
} else if (_engine->cfgfile.PolygonDetails == 1) {
menuSettings->setButtonTextId(i, TextId::kDetailsPolygonsMiddle);
} else {
menuSettings->setButtonTextId(i, TextId::kDetailsPolygonsHigh);
}
break;
case MenuButtonTypes::kShadowSettings:
if (_engine->cfgfile.ShadowMode == 0) {
menuSettings->setButtonTextId(i, TextId::kShadowsDisabled);
} else if (_engine->cfgfile.ShadowMode == 1) {
menuSettings->setButtonTextId(i, TextId::kShadowsFigures);
} else {
menuSettings->setButtonTextId(i, TextId::kDetailsShadowHigh);
}
break;
case MenuButtonTypes::kSceneryZoom:
if (_engine->cfgfile.SceZoom) {
menuSettings->setButtonTextId(i, TextId::kScenaryZoomOn);
} else {
menuSettings->setButtonTextId(i, TextId::kNoScenaryZoom);
}
break;
default:
break;
}
}
const int32 menuItemId = menuSettings->getButtonState(i);
const char *text = menuSettings->getButtonText(_engine->_text, i);
const int32 border = 45;
const int32 mainMenuButtonHeightHalf = 25;
const Common::Rect rect(border, topHeight - mainMenuButtonHeightHalf, SCREEN_WIDTH - border, topHeight + mainMenuButtonHeightHalf);
if (hover) {
if (i == buttonNumber) {
drawButtonGfx(menuSettings, rect, menuItemId, text, hover);
}
} else {
if (i == buttonNumber) {
drawButtonGfx(menuSettings, rect, menuItemId, text, true);
} else {
drawButtonGfx(menuSettings, rect, menuItemId, text, false);
}
}
if (_engine->_input->isMouseHovering(rect)) {
mouseActiveButton = i;
}
topHeight += 56; // increase button top height
}
return mouseActiveButton;
}
int32 Menu::processMenu(MenuSettings *menuSettings) {
int16 currentButton = menuSettings->getActiveButton();
bool buttonsNeedRedraw = true;
const int32 numEntry = menuSettings->getButtonCount();
int32 maxButton = numEntry - 1;
Common::Point mousepos = _engine->_input->getMousePositions();
bool useMouse = true;
_engine->_input->enableKeyMap(uiKeyMapId);
// if we are running the game already, the buttons are just rendered on top of the scene
if (_engine->_scene->isGameRunning()) {
_engine->_screens->copyScreen(_engine->workVideoBuffer, _engine->frontVideoBuffer);
_engine->flip();
} else {
_engine->_screens->loadMenuImage(false);
}
uint32 startMillis = _engine->_system->getMillis();
do {
ScopedFPS scopedFps;
const uint32 loopMillis = _engine->_system->getMillis();
_engine->readKeys();
Common::Point newmousepos = _engine->_input->getMousePositions();
if (mousepos != newmousepos) {
useMouse = true;
mousepos = newmousepos;
}
if (_engine->_input->toggleActionIfActive(TwinEActionType::UIDown)) {
currentButton++;
if (currentButton == numEntry) { // if current button is the last, than next button is the first
currentButton = 0;
}
useMouse = false;
buttonsNeedRedraw = true;
startMillis = loopMillis;
} else if (_engine->_input->toggleActionIfActive(TwinEActionType::UIUp)) {
currentButton--;
if (currentButton < 0) { // if current button is the first, than previous button is the last
currentButton = maxButton;
}
useMouse = false;
buttonsNeedRedraw = true;
startMillis = loopMillis;
}
const int16 id = menuSettings->getActiveButtonState();
if (menuSettings == &advOptionsMenuState) {
switch (id) {
case MenuButtonTypes::kAgressiveMode:
if (_engine->_input->toggleActionIfActive(TwinEActionType::UILeft) || _engine->_input->toggleActionIfActive(TwinEActionType::UIRight)) {
_engine->_actor->autoAgressive = !_engine->_actor->autoAgressive;
}
break;
case MenuButtonTypes::kPolygonDetails:
if (_engine->_input->toggleActionIfActive(TwinEActionType::UILeft)) {
_engine->cfgfile.PolygonDetails--;
_engine->cfgfile.PolygonDetails %= 3;
} else if (_engine->_input->toggleActionIfActive(TwinEActionType::UIRight)) {
_engine->cfgfile.PolygonDetails++;
_engine->cfgfile.PolygonDetails %= 3;
}
break;
case MenuButtonTypes::kShadowSettings:
if (_engine->_input->toggleActionIfActive(TwinEActionType::UILeft)) {
_engine->cfgfile.ShadowMode--;
_engine->cfgfile.ShadowMode %= 3;
} else if (_engine->_input->toggleActionIfActive(TwinEActionType::UIRight)) {
_engine->cfgfile.ShadowMode++;
_engine->cfgfile.ShadowMode %= 3;
}
break;
case MenuButtonTypes::kSceneryZoom:
if (_engine->_input->toggleActionIfActive(TwinEActionType::UILeft) || _engine->_input->toggleActionIfActive(TwinEActionType::UIRight)) {
_engine->cfgfile.SceZoom = !_engine->cfgfile.SceZoom;
}
break;
default:
break;
}
} else if (menuSettings == &volumeMenuState) {
Audio::Mixer *mixer = _engine->_system->getMixer();
switch (id) {
case MenuButtonTypes::kMusicVolume: {
int volume = mixer->getVolumeForSoundType(Audio::Mixer::SoundType::kMusicSoundType);
if (_engine->_input->isActionActive(TwinEActionType::UILeft)) {
volume -= 4;
} else if (_engine->_input->isActionActive(TwinEActionType::UIRight)) {
volume += 4;
}
_engine->_music->musicVolume(volume);
break;
}
case MenuButtonTypes::kSoundVolume: {
int volume = mixer->getVolumeForSoundType(Audio::Mixer::kSFXSoundType);
if (_engine->_input->isActionActive(TwinEActionType::UILeft)) {
volume -= 4;
} else if (_engine->_input->isActionActive(TwinEActionType::UIRight)) {
volume += 4;
}
mixer->setVolumeForSoundType(Audio::Mixer::kSFXSoundType, volume);
break;
}
case MenuButtonTypes::kCDVolume: {
AudioCDManager::Status status = _engine->_system->getAudioCDManager()->getStatus();
if (_engine->_input->isActionActive(TwinEActionType::UILeft)) {
status.volume -= 4;
} else if (_engine->_input->isActionActive(TwinEActionType::UIRight)) {
status.volume += 4;
}
status.volume = CLIP(status.volume, 0, 255);
_engine->_system->getAudioCDManager()->setVolume(status.volume);
break;
}
case MenuButtonTypes::kLineVolume: {
int volume = mixer->getVolumeForSoundType(Audio::Mixer::kSpeechSoundType);
if (_engine->_input->isActionActive(TwinEActionType::UILeft)) {
volume -= 4;
} else if (_engine->_input->isActionActive(TwinEActionType::UIRight)) {
volume += 4;
}
mixer->setVolumeForSoundType(Audio::Mixer::kSpeechSoundType, volume);
break;
}
case MenuButtonTypes::kMasterVolume: {
int volume = mixer->getVolumeForSoundType(Audio::Mixer::kPlainSoundType);
if (_engine->_input->isActionActive(TwinEActionType::UILeft)) {
volume -= 4;
} else if (_engine->_input->isActionActive(TwinEActionType::UIRight)) {
volume += 4;
}
mixer->setVolumeForSoundType(Audio::Mixer::kPlainSoundType, volume);
break;
}
default:
break;
}
}
if (buttonsNeedRedraw) {
// draw all buttons
const int16 mouseButtonHovered = drawButtons(menuSettings, false);
if (useMouse && mouseButtonHovered != -1) {
currentButton = mouseButtonHovered;
}
menuSettings->setActiveButton(currentButton);
}
// draw plasma effect for the current selected button
const int16 mouseButtonHovered = drawButtons(menuSettings, true);
if (useMouse && mouseButtonHovered != -1) {
if (mouseButtonHovered != currentButton) {
buttonsNeedRedraw = true;
}
currentButton = mouseButtonHovered;
}
if (_engine->shouldQuit()) {
return kQuitEngine;
}
if (_engine->_input->toggleActionIfActive(TwinEActionType::UIAbort)) {
for (int i = 0; i < menuSettings->getButtonCount(); ++i) {
const int16 textId = menuSettings->getButtonTextId(i);
if (textId == TextId::kReturnMenu || textId == TextId::kReturnGame || textId == TextId::kContinue) {
return textId;
}
}
startMillis = loopMillis;
}
if (loopMillis - startMillis > 15000) {
_engine->_menuOptions->showCredits();
startMillis = _engine->_system->getMillis();
_engine->_screens->loadMenuImage(false);
}
} while (!_engine->_input->toggleActionIfActive(TwinEActionType::UIEnter));
return menuSettings->getActiveButtonTextId();
}
int32 Menu::advoptionsMenu() {
_engine->_screens->copyScreen(_engine->workVideoBuffer, _engine->frontVideoBuffer);
_engine->flip();
ScopedCursor scoped(_engine);
for (;;) {
switch (processMenu(&advOptionsMenuState)) {
case TextId::kReturnMenu: {
return 0;
}
case kQuitEngine:
return kQuitEngine;
case TextId::kBehaviourAgressiveManual:
case TextId::kDetailsPolygonsHigh:
case TextId::kDetailsShadowHigh:
case TextId::kScenaryZoomOn:
default:
warning("Unknown menu button handled");
break;
}
}
return 0;
}
int32 Menu::savemanageMenu() {
_engine->_screens->copyScreen(_engine->workVideoBuffer, _engine->frontVideoBuffer);
_engine->flip();
ScopedCursor scoped(_engine);
for (;;) {
switch (processMenu(&saveManageMenuState)) {
case TextId::kReturnMenu:
return 0;
case TextId::kCreateSaveGame:
_engine->_menuOptions->saveGameMenu();
break;
case TextId::kDeleteSaveGame:
_engine->_menuOptions->deleteSaveMenu();
break;
case kQuitEngine:
return kQuitEngine;
default:
warning("Unknown menu button handled");
break;
}
}
return 0;
}
int32 Menu::volumeMenu() {
_engine->_screens->copyScreen(_engine->workVideoBuffer, _engine->frontVideoBuffer);
_engine->flip();
ScopedCursor scoped(_engine);
for (;;) {
switch (processMenu(&volumeMenuState)) {
case TextId::kReturnMenu:
return 0;
case TextId::kSaveSettings:
// TODO: implement setting persisting
break;
case kQuitEngine:
return kQuitEngine;
case TextId::kMusicVolume:
case TextId::kSoundVolume:
case TextId::kCDVolume:
case TextId::kLineInVolume:
case TextId::kMasterVolume:
default:
warning("Unknown menu button handled");
break;
}
}
return 0;
}
void Menu::inGameOptionsMenu() {
_engine->_text->initTextBank(TextBankId::Options_and_menus);
optionsMenuState.setButtonTextId(0, TextId::kReturnGame);
_engine->_screens->copyScreen(_engine->frontVideoBuffer, _engine->workVideoBuffer);
optionsMenu();
_engine->_text->initSceneTextBank();
optionsMenuState.setButtonTextId(0, TextId::kReturnMenu);
}
int32 Menu::optionsMenu() {
_engine->_screens->copyScreen(_engine->workVideoBuffer, _engine->frontVideoBuffer);
_engine->flip();
_engine->_sound->stopSamples();
//_engine->_music->playTrackMusic(9);
ScopedCursor scoped(_engine);
for (;;) {
switch (processMenu(&optionsMenuState)) {
case TextId::kReturnGame:
case TextId::kReturnMenu: {
return 0;
}
case TextId::kVolumeSettings: {
checkMenuQuit(volumeMenu()) break;
}
case TextId::kSaveManage: {
checkMenuQuit(savemanageMenu()) break;
}
case TextId::kAdvanced: {
checkMenuQuit(advoptionsMenu()) break;
}
case kQuitEngine:
return kQuitEngine;
default:
break;
}
}
return 0;
}
static const byte cursorArrow[] = {
1, 1, 3, 3, 3, 3, 3, 3, 3, 3, 3,
1, 0, 1, 3, 3, 3, 3, 3, 3, 3, 3,
1, 0, 0, 1, 3, 3, 3, 3, 3, 3, 3,
1, 0, 0, 0, 1, 3, 3, 3, 3, 3, 3,
1, 0, 0, 0, 0, 1, 3, 3, 3, 3, 3,
1, 0, 0, 0, 0, 0, 1, 3, 3, 3, 3,
1, 0, 0, 0, 0, 0, 0, 1, 3, 3, 3,
1, 0, 0, 0, 0, 0, 0, 0, 1, 3, 3,
1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 3,
1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1,
1, 0, 0, 1, 0, 0, 1, 3, 3, 3, 3,
1, 0, 1, 3, 1, 0, 0, 1, 3, 3, 3,
1, 1, 3, 3, 1, 0, 0, 1, 3, 3, 3,
1, 3, 3, 3, 3, 1, 0, 0, 1, 3, 3,
3, 3, 3, 3, 3, 1, 0, 0, 1, 3, 3,
3, 3, 3, 3, 3, 3, 1, 1, 1, 3, 3};
static const byte cursorPalette[] = {
0, 0, 0,
0xff, 0xff, 0xff};
bool Menu::init() {
// load menu effect file only once
plasmaEffectPtr = (uint8 *)malloc(kPlasmaEffectFilesize);
memset(plasmaEffectPtr, 0, kPlasmaEffectFilesize);
CursorMan.pushCursor(cursorArrow, 11, 16, 1, 1, 3);
CursorMan.pushCursorPalette(cursorPalette, 0, 2);
return HQR::getEntry(plasmaEffectPtr, Resources::HQR_RESS_FILE, RESSHQR_PLASMAEFFECT) > 0;
}
EngineState Menu::run() {
ScopedFPS scopedFps;
_engine->_text->initTextBank(TextBankId::Options_and_menus);
_engine->_music->playTrackMusic(9); // LBA's Theme
_engine->_sound->stopSamples();
ScopedCursor scoped(_engine);
switch (processMenu(&mainMenuState)) {
case TextId::kNewGame: {
if (_engine->_menuOptions->newGameMenu()) {
return EngineState::GameLoop;
}
break;
}
case TextId::kContinueGame: {
if (_engine->_menuOptions->continueGameMenu()) {
return EngineState::LoadedGame;
}
break;
}
case TextId::kOptions: {
optionsMenu();
break;
}
case kBackground: {
_engine->_screens->loadMenuImage();
break;
}
case TextId::kQuit:
case kQuitEngine:
debug("quit the game");
return EngineState::QuitGame;
}
return EngineState::Menu;
}
int32 Menu::giveupMenu() {
_engine->_screens->copyScreen(_engine->frontVideoBuffer, _engine->workVideoBuffer);
_engine->_sound->pauseSamples();
MenuSettings *localMenu;
if (_engine->cfgfile.UseAutoSaving) {
localMenu = &giveUpMenuState;
} else {
localMenu = &giveUpMenuWithSaveState;
}
ScopedCursor scoped(_engine);
int32 menuId;
do {
ScopedFPS scopedFps;
_engine->_text->initTextBank(TextBankId::Options_and_menus);
menuId = processMenu(localMenu);
switch (menuId) {
case TextId::kContinue:
_engine->_sound->resumeSamples();
break;
case TextId::kGiveUp:
_engine->_gameState->giveUp();
return 1;
case TextId::kCreateSaveGame:
_engine->_menuOptions->saveGameMenu();
break;
case kQuitEngine:
return kQuitEngine;
default:
warning("Unknown menu button handled: %i", menuId);
}
_engine->_text->initSceneTextBank();
} while (menuId != TextId::kGiveUp && menuId != TextId::kContinue && menuId != TextId::kCreateSaveGame);
return 0;
}
void Menu::drawInfoMenu(int16 left, int16 top) {
_engine->_interface->resetClip();
const int32 width = 450;
const int32 height = 80;
const Common::Rect rect(left, top, left + width, top + height);
drawBox(rect);
Common::Rect splittedBoxRect(rect);
splittedBoxRect.grow(-1);
_engine->_interface->drawSplittedBox(splittedBoxRect, 0);
int32 newBoxLeft2 = left + 9;
_engine->_grid->drawSprite(0, newBoxLeft2, top + 13, _engine->_resources->spriteTable[SPRITEHQR_LIFEPOINTS]);
int32 boxRight = left + 325;
int32 newBoxLeft = left + 25;
int32 boxLeft = _engine->_screens->crossDot(newBoxLeft, boxRight, 50, _engine->_scene->sceneHero->life);
int32 boxTop = top + 10;
int32 boxBottom = top + 25;
_engine->_interface->drawSplittedBox(Common::Rect(newBoxLeft, boxTop, boxLeft, boxBottom), 91);
drawBox(newBoxLeft, boxTop, left + 324, boxTop + 14);
if (!_engine->_gameState->inventoryDisabled() && _engine->_gameState->hasItem(InventoryItems::kiTunic)) {
_engine->_grid->drawSprite(0, newBoxLeft2, top + 36, _engine->_resources->spriteTable[SPRITEHQR_MAGICPOINTS]);
if (_engine->_gameState->magicLevelIdx > 0) {
_engine->_interface->drawSplittedBox(Common::Rect(newBoxLeft, top + 35, _engine->_screens->crossDot(newBoxLeft, boxRight, 80, _engine->_gameState->inventoryMagicPoints), top + 50), 75);
}
drawBox(newBoxLeft, top + 35, left + _engine->_gameState->magicLevelIdx * 80 + 20, top + 35 + 15);
}
boxLeft = left + 340;
/** draw coin sprite */
_engine->_grid->drawSprite(0, boxLeft, top + 15, _engine->_resources->spriteTable[SPRITEHQR_KASHES]);
_engine->_text->setFontColor(155);
Common::String inventoryNumKashes = Common::String::format("%d", _engine->_gameState->inventoryNumKashes);
_engine->_text->drawText(left + 370, top + 5, inventoryNumKashes.c_str());
/** draw key sprite */
_engine->_grid->drawSprite(0, boxLeft, top + 55, _engine->_resources->spriteTable[SPRITEHQR_KEY]);
_engine->_text->setFontColor(155);
Common::String inventoryNumKeys = Common::String::format("%d", _engine->_gameState->inventoryNumKeys);
_engine->_text->drawText(left + 370, top + 40, inventoryNumKeys.c_str());
// prevent
if (_engine->_gameState->inventoryNumLeafs > _engine->_gameState->inventoryNumLeafsBox) {
_engine->_gameState->inventoryNumLeafs = _engine->_gameState->inventoryNumLeafsBox;
}
// Clover leaf boxes
for (int32 i = 0; i < _engine->_gameState->inventoryNumLeafsBox; i++) {
_engine->_grid->drawSprite(0, _engine->_screens->crossDot(left + 25, left + 325, 10, i), top + 58, _engine->_resources->spriteTable[SPRITEHQR_CLOVERLEAFBOX]);
}
// Clover leafs
for (int32 i = 0; i < _engine->_gameState->inventoryNumLeafs; i++) {
_engine->_grid->drawSprite(0, _engine->_screens->crossDot(left + 25, left + 325, 10, i) + 2, top + 60, _engine->_resources->spriteTable[SPRITEHQR_CLOVERLEAF]);
}
_engine->copyBlockPhys(left, top, left + width, top + 135);
}
Common::Rect Menu::calcBehaviourRect(HeroBehaviourType behaviour) const {
const int border = 110;
const int32 padding = 11;
const int32 width = 99;
const int height = 119;
const int32 boxLeft = (int32)behaviour * (width + padding) + border;
const int32 boxRight = boxLeft + width;
const int32 boxTop = border;
const int32 boxBottom = boxTop + height;
return Common::Rect(boxLeft, boxTop, boxRight, boxBottom);
}
bool Menu::isBehaviourHovered(HeroBehaviourType behaviour) const {
const Common::Rect &boxRect = calcBehaviourRect(behaviour);
return _engine->_input->isMouseHovering(boxRect);
}
void Menu::drawBehaviour(HeroBehaviourType behaviour, int32 angle, bool cantDrawBox) {
const Common::Rect &boxRect = calcBehaviourRect(behaviour);
const int titleOffset = 10;
const int titleHeight = 40;
const int32 titleBoxLeft = 110;
const int32 titleBoxRight = 540;
const int32 titleBoxTop = boxRect.bottom + titleOffset;
const int32 titleBoxBottom = titleBoxTop + titleHeight;
const Common::Rect titleRect(titleBoxLeft, titleBoxTop, titleBoxRight, titleBoxBottom);
const uint8 *currentAnim = _engine->_resources->animTable[_engine->_actor->heroAnimIdx[(byte)behaviour]];
int16 currentAnimState = behaviourAnimState[(byte)behaviour];
if (_engine->_animations->setModelAnimation(currentAnimState, currentAnim, behaviourEntity, &behaviourAnimData[(byte)behaviour])) {
currentAnimState++; // keyframe
if (currentAnimState >= _engine->_animations->getNumKeyframes(currentAnim)) {
currentAnimState = _engine->_animations->getStartKeyframe(currentAnim);
}
behaviourAnimState[(byte)behaviour] = currentAnimState;
}
if (!cantDrawBox) {
Common::Rect boxRectCopy(boxRect);
boxRectCopy.grow(1);
drawBox(boxRectCopy);
}
_engine->_interface->saveClip();
_engine->_interface->resetClip();
if (behaviour != _engine->_actor->heroBehaviour) { // unselected
_engine->_interface->drawSplittedBox(boxRect, 0);
} else { // selected
_engine->_interface->drawSplittedBox(boxRect, 69);
// behaviour menu title
_engine->_interface->drawSplittedBox(titleRect, 0);
drawBox(titleRect);
_engine->_text->setFontColor(15);
char dialText[256];
_engine->_text->getMenuText(_engine->_actor->getTextIdForBehaviour(), dialText, sizeof(dialText));
_engine->_text->drawText(SCREEN_WIDTH / 2 - _engine->_text->getTextSize(dialText) / 2, titleBoxTop + 1, dialText);
}
_engine->_renderer->renderBehaviourModel(boxRect, -600, angle, behaviourEntity);
_engine->copyBlockPhys(boxRect);
_engine->copyBlockPhys(titleRect);
_engine->_interface->loadClip();
}
void Menu::prepareAndDrawBehaviour(int32 angle, HeroBehaviourType behaviour) {
_engine->_animations->setAnimAtKeyframe(behaviourAnimState[(byte)behaviour], _engine->_resources->animTable[_engine->_actor->heroAnimIdx[(byte)behaviour]], behaviourEntity, &behaviourAnimData[(byte)behaviour]);
drawBehaviour(behaviour, angle, false);
}
void Menu::drawBehaviourMenu(int32 angle) {
const Common::Rect titleRect(100, 100, 550, 290);
drawBox(titleRect);
Common::Rect boxRect(titleRect);
boxRect.grow(-1);
_engine->_interface->drawTransparentBox(boxRect, 2);
prepareAndDrawBehaviour(angle, HeroBehaviourType::kNormal);
prepareAndDrawBehaviour(angle, HeroBehaviourType::kAthletic);
prepareAndDrawBehaviour(angle, HeroBehaviourType::kAggressive);
prepareAndDrawBehaviour(angle, HeroBehaviourType::kDiscrete);
drawInfoMenu(titleRect.left, titleRect.bottom + 10);
_engine->copyBlockPhys(titleRect);
}
void Menu::processBehaviourMenu() {
if (_engine->_actor->heroBehaviour == HeroBehaviourType::kProtoPack) {
_engine->_sound->stopSamples();
_engine->_actor->setBehaviour(HeroBehaviourType::kNormal);
}
behaviourEntity = _engine->_actor->bodyTable[_engine->_scene->sceneHero->entity];
_engine->_actor->heroAnimIdx[(byte)HeroBehaviourType::kNormal] = _engine->_actor->heroAnimIdxNORMAL;
_engine->_actor->heroAnimIdx[(byte)HeroBehaviourType::kAthletic] = _engine->_actor->heroAnimIdxATHLETIC;
_engine->_actor->heroAnimIdx[(byte)HeroBehaviourType::kAggressive] = _engine->_actor->heroAnimIdxAGGRESSIVE;
_engine->_actor->heroAnimIdx[(byte)HeroBehaviourType::kDiscrete] = _engine->_actor->heroAnimIdxDISCRETE;
_engine->_movements->setActorAngleSafe(_engine->_scene->sceneHero->angle, _engine->_scene->sceneHero->angle - ANGLE_90, 50, &moveMenu);
_engine->_screens->copyScreen(_engine->frontVideoBuffer, _engine->workVideoBuffer);
int32 tmpTextBank = _engine->_scene->sceneTextBank;
_engine->_scene->sceneTextBank = TextBankId::None;
_engine->_text->initTextBank(TextBankId::Options_and_menus);
drawBehaviourMenu(_engine->_scene->sceneHero->angle);
HeroBehaviourType tmpHeroBehaviour = _engine->_actor->heroBehaviour;
_engine->_animations->setAnimAtKeyframe(behaviourAnimState[(byte)_engine->_actor->heroBehaviour], _engine->_resources->animTable[_engine->_actor->heroAnimIdx[(byte)_engine->_actor->heroBehaviour]], behaviourEntity, &behaviourAnimData[(byte)_engine->_actor->heroBehaviour]);
int32 tmpTime = _engine->lbaTime;
#if 0
ScopedCursor scopedCursor(_engine);
#endif
ScopedKeyMap scopedKeyMap(_engine, uiKeyMapId);
while (_engine->_input->isActionActive(TwinEActionType::BehaviourMenu) || _engine->_input->isQuickBehaviourActionActive()) {
ScopedFPS scopedFps(50);
_engine->readKeys();
#if 0
if (isBehaviourHovered(HeroBehaviourType::kNormal)) {
_engine->_actor->heroBehaviour = HeroBehaviourType::kNormal;
} else if (isBehaviourHovered(HeroBehaviourType::kAthletic)) {
_engine->_actor->heroBehaviour = HeroBehaviourType::kAthletic;
} else if (isBehaviourHovered(HeroBehaviourType::kAggressive)) {
_engine->_actor->heroBehaviour = HeroBehaviourType::kAggressive;
} else if (isBehaviourHovered(HeroBehaviourType::kDiscrete)) {
_engine->_actor->heroBehaviour = HeroBehaviourType::kDiscrete;
}
#endif
int heroBehaviour = (int)_engine->_actor->heroBehaviour;
if (_engine->_input->toggleActionIfActive(TwinEActionType::UILeft)) {
heroBehaviour--;
} else if (_engine->_input->toggleActionIfActive(TwinEActionType::UIRight)) {
heroBehaviour++;
}
if (heroBehaviour < (int)HeroBehaviourType::kNormal) {
heroBehaviour = (int)HeroBehaviourType::kDiscrete;
} else if (heroBehaviour >= (int)HeroBehaviourType::kProtoPack) {
heroBehaviour = (int)HeroBehaviourType::kNormal;
}
_engine->_actor->heroBehaviour = (HeroBehaviourType)heroBehaviour;
if (tmpHeroBehaviour != _engine->_actor->heroBehaviour) {
drawBehaviour(tmpHeroBehaviour, _engine->_scene->sceneHero->angle, true);
tmpHeroBehaviour = _engine->_actor->heroBehaviour;
_engine->_movements->setActorAngleSafe(_engine->_scene->sceneHero->angle, _engine->_scene->sceneHero->angle - ANGLE_90, 50, &moveMenu);
_engine->_animations->setAnimAtKeyframe(behaviourAnimState[(byte)_engine->_actor->heroBehaviour], _engine->_resources->animTable[_engine->_actor->heroAnimIdx[(byte)_engine->_actor->heroBehaviour]], behaviourEntity, &behaviourAnimData[(byte)_engine->_actor->heroBehaviour]);
}
drawBehaviour(_engine->_actor->heroBehaviour, -1, true);
_engine->lbaTime++;
}
_engine->lbaTime = tmpTime;
_engine->_actor->setBehaviour(_engine->_actor->heroBehaviour);
_engine->_gameState->initEngineProjections();
_engine->_scene->sceneTextBank = tmpTextBank;
_engine->_text->initSceneTextBank();
}
void Menu::drawMagicItemsBox(int32 left, int32 top, int32 right, int32 bottom, int32 color) { // Rect
_engine->_interface->drawLine(left, top, right, top, color); // top line
_engine->_interface->drawLine(left, top, left, bottom, color); // left line
_engine->_interface->drawLine(right, ++top, right, bottom, color); // right line
_engine->_interface->drawLine(++left, bottom, right, bottom, color); // bottom line
}
void Menu::drawItem(int32 item) {
const int32 itemX = (item / 4) * 85 + 64;
const int32 itemY = (item & 3) * 75 + 52;
const int32 left = itemX - 37;
const int32 right = itemX + 37;
const int32 top = itemY - 32;
const int32 bottom = itemY + 32;
const Common::Rect rect(left, top, right, bottom);
_engine->_interface->drawSplittedBox(rect, inventorySelectedItem == item ? inventorySelectedColor : 0);
if (item < NUM_INVENTORY_ITEMS && _engine->_gameState->hasItem((InventoryItems)item) && !_engine->_gameState->inventoryDisabled()) {
Renderer::prepareIsoModel(_engine->_resources->inventoryTable[item]);
itemAngle[item] += 8;
_engine->_renderer->renderInventoryItem(itemX, itemY, _engine->_resources->inventoryTable[item], itemAngle[item], 15000);
if (item == InventoryItems::kGasItem) { // has GAS
_engine->_text->setFontColor(15);
Common::String inventoryNumGas = Common::String::format("%d", _engine->_gameState->inventoryNumGas);
_engine->_text->drawText(left + 3, top + 32, inventoryNumGas.c_str());
}
}
drawBox(rect);
_engine->copyBlockPhys(rect);
}
void Menu::drawInventoryItems() {
const Common::Rect rect(17, 10, 622, 320);
_engine->_interface->drawTransparentBox(rect, 4);
drawBox(rect);
drawMagicItemsBox(110, 18, 188, 311, 75);
_engine->copyBlockPhys(rect);
for (int32 item = 0; item < NUM_INVENTORY_ITEMS; item++) {
drawItem(item);
}
}
void Menu::processInventoryMenu() {
int32 tmpAlphaLight = _engine->_scene->alphaLight;
int32 tmpBetaLight = _engine->_scene->betaLight;
_engine->_screens->copyScreen(_engine->frontVideoBuffer, _engine->workVideoBuffer);
_engine->_renderer->setLightVector(896, 950, 0);
inventorySelectedColor = 68;
if (_engine->_gameState->inventoryNumLeafs > 0) {
_engine->_gameState->giveItem(InventoryItems::kiCloverLeaf);
// TODO: shouldn't this get reset? } else {
// _engine->_gameState->removeItem(InventoryItems::kiCloverLeaf);
}
drawInventoryItems();
_engine->_text->initTextBank(TextBankId::Inventory_Intro_and_Holomap);
bool updateItemText = true;
_engine->_text->setFontCrossColor(4);
_engine->_text->initDialogueBox();
ProgressiveTextState textStatus = ProgressiveTextState::End;
#if 0
ScopedCursor scopedCursor(_engine);
#endif
ScopedKeyMap scopedKeyMap(_engine, uiKeyMapId);
for (;;) {
ScopedFPS fps(66);
_engine->readKeys();
int32 prevSelectedItem = inventorySelectedItem;
if (_engine->_input->toggleAbortAction() || _engine->shouldQuit()) {
break;
}
const bool cursorDown = _engine->_input->toggleActionIfActive(TwinEActionType::UIDown);
const bool cursorUp = _engine->_input->toggleActionIfActive(TwinEActionType::UIUp);
const bool cursorLeft = _engine->_input->toggleActionIfActive(TwinEActionType::UILeft);
const bool cursorRight = _engine->_input->toggleActionIfActive(TwinEActionType::UIRight);
if (cursorDown) {
inventorySelectedItem++;
if (inventorySelectedItem >= NUM_INVENTORY_ITEMS) {
inventorySelectedItem = 0;
}
drawItem(prevSelectedItem);
updateItemText = true;
} else if (cursorUp) {
inventorySelectedItem--;
if (inventorySelectedItem < 0) {
inventorySelectedItem = NUM_INVENTORY_ITEMS - 1;
}
drawItem(prevSelectedItem);
updateItemText = true;
} else if (cursorLeft) {
inventorySelectedItem -= 4;
if (inventorySelectedItem < 0) {
inventorySelectedItem += NUM_INVENTORY_ITEMS;
}
drawItem(prevSelectedItem);
updateItemText = true;
} else if (cursorRight) {
inventorySelectedItem += 4;
if (inventorySelectedItem >= NUM_INVENTORY_ITEMS) {
inventorySelectedItem -= NUM_INVENTORY_ITEMS;
}
drawItem(prevSelectedItem);
updateItemText = true;
}
if (updateItemText) {
_engine->_text->initInventoryDialogueBox();
if (inventorySelectedItem < NUM_INVENTORY_ITEMS && _engine->_gameState->hasItem((InventoryItems)inventorySelectedItem) && !_engine->_gameState->inventoryDisabled()) {
_engine->_text->initInventoryText(inventorySelectedItem);
} else {
_engine->_text->initInventoryText(NUM_INVENTORY_ITEMS);
}
}
if (updateItemText || textStatus != ProgressiveTextState::NextPage) {
textStatus = _engine->_text->updateProgressiveText();
updateItemText = false;
}
if (_engine->_input->toggleActionIfActive(TwinEActionType::UINextPage)) {
if (textStatus == ProgressiveTextState::NextPage) {
_engine->_text->initInventoryDialogueBox();
textStatus = ProgressiveTextState::End;
} else {
if (inventorySelectedItem < NUM_INVENTORY_ITEMS && _engine->_gameState->hasItem((InventoryItems)inventorySelectedItem) && !_engine->_gameState->inventoryDisabled()) {
_engine->_text->initInventoryDialogueBox();
_engine->_text->initInventoryText(inventorySelectedItem);
}
}
}
drawItem(inventorySelectedItem);
if (inventorySelectedItem < NUM_INVENTORY_ITEMS && _engine->_input->toggleActionIfActive(TwinEActionType::UIEnter) && _engine->_gameState->hasItem((InventoryItems)inventorySelectedItem) && !_engine->_gameState->inventoryDisabled()) {
_engine->loopInventoryItem = inventorySelectedItem;
inventorySelectedColor = 91;
drawItem(inventorySelectedItem);
break;
}
}
_engine->_text->_hasValidTextHandle = false;
_engine->_scene->alphaLight = tmpAlphaLight;
_engine->_scene->betaLight = tmpBetaLight;
_engine->_gameState->initEngineProjections();
_engine->_text->initSceneTextBank();
}
} // namespace TwinE