diff --git a/projects/mtg/bin/Res/graphics/DeckMenuBackdrop.png b/projects/mtg/bin/Res/graphics/DeckMenuBackdrop.png new file mode 100644 index 000000000..8d8ddb8b0 Binary files /dev/null and b/projects/mtg/bin/Res/graphics/DeckMenuBackdrop.png differ diff --git a/projects/mtg/include/DeckMenu.h b/projects/mtg/include/DeckMenu.h new file mode 100644 index 000000000..8e053284d --- /dev/null +++ b/projects/mtg/include/DeckMenu.h @@ -0,0 +1,53 @@ +/* + A class for very simple menus structure +*/ +#ifndef _DeckMenu_H_ +#define _DeckMenu_H_ + +#include +#include +#include "WFont.h" +#include "hge/hgeparticle.h" +#include "DeckMetaData.h" + + +class DeckMenu:public JGuiController{ + private: + int mHeight, mWidth, mX, mY; + int titleX, titleY, titleWidth; + int descX, descY, descHeight, descWidth; + int statsX, statsY, statsHeight, statsWidth; + + int fontId; + std::string title; + int displaytitle; + int maxItems, startId; + float selectionT, selectionY; + float timeOpen; + static unsigned int refCount; + + JQuad *background; + JTexture *backgroundTexture; + static WFont* titleFont; + static hgeParticleSystem* stars; + // This works only because of no multithreading + static PIXEL_TYPE jewelGraphics[9]; + + inline void MogrifyJewel(); + + public: + + bool autoTranslate; + DeckMenu(int id, JGuiListener* listener, int fontId, const char * _title = ""); + void Render(); + void Update(float dt); + void Add(int id, const char * Text, string desc = "", bool forceFocus = false, DeckMetaData *deckMetaData = NULL); + void Close(); + + float selectionTargetY; + bool closed; + static void destroy(); +}; + + +#endif diff --git a/projects/mtg/include/DeckMenuItem.h b/projects/mtg/include/DeckMenuItem.h new file mode 100644 index 000000000..3c91685b4 --- /dev/null +++ b/projects/mtg/include/DeckMenuItem.h @@ -0,0 +1,50 @@ +#ifndef _DECKMENU_ITEM_H +#define _DECKMENU_ITEM_H + +#include +#include +#include +#include "DeckMenu.h" + +using std::string; + +#define SCALE_SELECTED 1.2f +#define SCALE_NORMAL 1.0f + + +class DeckMenuItem: public JGuiObject +{ + private: + bool mHasFocus; + DeckMenu* parent; + int fontId; + string mText; + float mScale; + float mTargetScale; + + public: + string imageFilename; + string desc; + DeckMetaData *meta; + + DeckMenuItem(DeckMenu* _parent, int id, int fontId, string text, int x, int y, bool hasFocus = false, bool autoTranslate = false, DeckMetaData *meta = NULL); + ~DeckMenuItem(); + int mX; + int mY; + + void Relocate(int x, int y); + int GetWidth(); + bool hasFocus(); + + void RenderWithOffset(float yOffset); + virtual void Render(); + virtual void Update(float dt); + + virtual void Entering(); + virtual bool Leaving(JButton key); + virtual bool ButtonPressed(); + virtual ostream& toString(ostream& out) const; + virtual bool getTopLeft(int& top, int& left) {top = mY; left = mX; return true;}; +}; + +#endif diff --git a/projects/mtg/include/DeckMetaData.h b/projects/mtg/include/DeckMetaData.h index 6101d0243..1d5aab675 100644 --- a/projects/mtg/include/DeckMetaData.h +++ b/projects/mtg/include/DeckMetaData.h @@ -22,6 +22,7 @@ private: string _desc; string _name; int _deckid; + string _avatarFilename; // statistical information @@ -37,11 +38,15 @@ public: string getFilename(); string getDescription(); string getName(); + string getAvatarFilename(); + string getStatsSummary(); + int getDeckId(); int getGamesPlayed(); int getVictories(); int getVictoryPercentage(); int getDifficulty(); + string getDifficultyString(); }; diff --git a/projects/mtg/include/GameState.h b/projects/mtg/include/GameState.h index e312b4eaa..2eaaad569 100644 --- a/projects/mtg/include/GameState.h +++ b/projects/mtg/include/GameState.h @@ -10,6 +10,7 @@ class JGE; #include #include #include "DeckMetaData.h" +#include "DeckMenu.h" using namespace std; @@ -66,13 +67,19 @@ class GameState // generate the Deck Meta Data and build the menu items of the menu given static vector fillDeckMenu(SimpleMenu * _menu, string path, string smallDeckPrefix = "", Player * statsPlayer = NULL); + + // generate the Deck Meta Data and build the menu items of the menu given + static vector fillDeckMenu(DeckMenu * _menu, string path, string smallDeckPrefix = "", Player * statsPlayer = NULL); // build a vector of decks with the information passsed in. static vector getValidDeckMetaData(string path, string smallDeckPrefix = "", Player * statsPlayer = NULL); // build menu items based on the vector static void renderDeckMenu(SimpleMenu * _menu, vector deckMetaDataList); - + + // build menu items based on the vector + static void renderDeckMenu(DeckMenu * _menu, vector deckMetaDataList); + }; bool sortByName( DeckMetaData * d1, DeckMetaData * d2 ); diff --git a/projects/mtg/include/GameStateDeckViewer.h b/projects/mtg/include/GameStateDeckViewer.h index 23b723384..d03763d5d 100644 --- a/projects/mtg/include/GameStateDeckViewer.h +++ b/projects/mtg/include/GameStateDeckViewer.h @@ -8,6 +8,7 @@ #include "GameState.h" #include "SimpleMenu.h" +#include "DeckMenu.h" #include "WResourceManager.h" #include "CardGui.h" #include "GameOptions.h" diff --git a/projects/mtg/include/GameStateDuel.h b/projects/mtg/include/GameStateDuel.h index 4954fd514..b4f3edb1e 100644 --- a/projects/mtg/include/GameStateDuel.h +++ b/projects/mtg/include/GameStateDuel.h @@ -4,6 +4,7 @@ #include "GameState.h" #include "SimpleMenu.h" +#include "DeckMenu.h" #include "MTGDeck.h" #include "GameObserver.h" @@ -28,8 +29,8 @@ class GameStateDuel: public GameState, public JGuiListener Player * mPlayers[2]; MTGPlayerCards * deck[2]; GameObserver * game; - SimpleMenu * deckmenu; - SimpleMenu * opponentMenu; + DeckMenu * deckmenu; + DeckMenu * opponentMenu; SimpleMenu * menu; bool premadeDeck; int OpponentsDeckid; diff --git a/projects/mtg/src/DeckMenu.cpp b/projects/mtg/src/DeckMenu.cpp new file mode 100644 index 000000000..41e7cc797 --- /dev/null +++ b/projects/mtg/src/DeckMenu.cpp @@ -0,0 +1,208 @@ +#include "PrecompiledHeader.h" + +#include "DeckMenu.h" +#include "DeckMenuItem.h" +#include "DeckMetaData.h" +#include "JTypes.h" +#include "GameApp.h" +#include "Translate.h" + +namespace +{ + const unsigned int kPoleWidth = 7; + const unsigned int kVerticalMargin = 16; + const unsigned int kHorizontalMargin = 30; + const signed int kLineHeight = 20; +} + +WFont* DeckMenu::titleFont = NULL; +hgeParticleSystem* DeckMenu::stars = NULL; +unsigned int DeckMenu::refCount = 0; +// Here comes the magic of jewel graphics +PIXEL_TYPE DeckMenu::jewelGraphics[9] = {0x3FFFFFFF,0x63645AEA,0x610D0D98, + 0x63645AEA,0xFF635AD5,0xFF110F67, + 0x610D0D98,0xFF110F67,0xFD030330}; + +// +// For the additional info window, maximum characters per line is roughly 30 characters across. +// TODO: figure a way to get incoming text to wrap. +// +// used fixed locations where the menu, title and descriptive text are located. +// * menu at (125, 60 ) +// * descriptive information 125 + +DeckMenu::DeckMenu(int id, JGuiListener* listener, int fontId, const char * _title) +: JGuiController(id, listener), +fontId(fontId) { + + background = NULL; + autoTranslate = true; + maxItems = 7; + mHeight = 2 * kVerticalMargin + ( maxItems * kLineHeight ); + mWidth = 0; + mX = 125; + mY = 60; + + // where to place the title of the menu + titleX = mX; + titleY = mY - 30; + title = _(_title); + + // where stats information goes + statsX = 280; + statsY = 8 + kVerticalMargin; + statsHeight = 50; + statsWidth = SCREEN_WIDTH / 2 - 40; // 40 is the width of the right border + + // where to place the descripiton information + descX = 229; + descY = 70; + + startId = 0; + selectionT = 0; + timeOpen = 0; + closed = false; + ++refCount; + selectionTargetY = selectionY = kVerticalMargin; + + if (NULL == stars) + stars = NEW hgeParticleSystem(resources.RetrievePSI("stars.psi", resources.GetQuad("stars"))); + + stars->FireAt(mX, mY); +} + +// TODO: Make this configurable, perhaps by user as part of the theme options. +JQuad* getBackground() +{ + resources.RetrieveTexture("DeckMenuBackdrop.png", RETRIEVE_MANAGE ); + return resources.RetrieveQuad("DeckMenuBackdrop.png", 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, "DualPaneBG" ); + +} + + +void DeckMenu::Render() { + JRenderer * renderer = JRenderer::GetInstance(); + + WFont * titleFont = resources.GetWFont(Fonts::SMALLFACE_FONT); + WFont * mFont = resources.GetWFont(fontId); + + // figure out where to place the stars initially + if (0 == mWidth) { + float sY = mY + kVerticalMargin; + for (int i = startId; i < startId + mCount; ++i) { + DeckMenuItem *menuItem = static_cast (mObjects[i]); + int width = menuItem->GetWidth(); + if (mWidth < width) mWidth = width; + } + if ((!title.empty()) && (mWidth < titleFont->GetStringWidth(title.c_str()))) + mWidth = titleFont->GetStringWidth(title.c_str()); + mWidth += 2*kHorizontalMargin; + for (int i = startId; i < startId + mCount; ++i) { + float y = mY + kVerticalMargin + i * kLineHeight; + DeckMenuItem * currentMenuItem = static_cast(mObjects[i]); + currentMenuItem->Relocate( mX, y); + + if (currentMenuItem->hasFocus()) sY = y; + } + stars->Fire(); + selectionTargetY = selectionY = sY; + timeOpen = 0; + } + + + renderer->RenderQuad(getBackground(), 0, 0 ); + + float height = mHeight; + if (timeOpen < 1) height *= timeOpen > 0 ? timeOpen : -timeOpen; + + renderer->SetTexBlend(BLEND_SRC_ALPHA, BLEND_ONE); + stars->Render(); + renderer->SetTexBlend(BLEND_SRC_ALPHA, BLEND_ONE_MINUS_SRC_ALPHA); + + mFont->SetScale(1.0f); + + for (int i = startId; i < startId + maxItems ; i++){ + if (i > mCount-1) break; + if ( static_cast(mObjects[i])->mY - kLineHeight * startId < mY + height - kLineHeight + 7) { + DeckMenuItem *currentMenuItem = static_cast(mObjects[i]); + if ( currentMenuItem->hasFocus()){ + // display the avatar image + if ( currentMenuItem->imageFilename.size() > 0 ) + { + JQuad * quad = resources.RetrieveTempQuad( currentMenuItem->imageFilename, TEXTURE_SUB_AVATAR ); + if (quad) { + renderer->RenderQuad(quad, 232, 10, 0, 0.9f, 0.9f); + } + } + // fill in the description part of the screen + string text = currentMenuItem->desc; + WFont *mainFont = resources.GetWFont(Fonts::MAIN_FONT); + mainFont->DrawString(text.c_str(), descX, descY); + mFont->SetColor(ARGB(255,255,255,0)); + + // fill in the statistical portion + if ( currentMenuItem->meta ) + { + ostringstream oss; + oss << "Deck: " << currentMenuItem->meta->getName() << endl; + oss << currentMenuItem->meta->getStatsSummary(); + + mainFont->DrawString( oss.str(), statsX, statsY - kVerticalMargin ); + } + + // fill in the bottom section of the screen. + // TODO: add text scroller of Tasks. + } + else { + mFont->SetColor(ARGB(150,255,255,255)); + } + currentMenuItem->RenderWithOffset(-kLineHeight*startId); + } + if (!title.empty()) + titleFont->DrawString(title.c_str(), titleX, titleY, JGETEXT_CENTER); + + } +} + +void DeckMenu::Update(float dt){ + JGuiController::Update(dt); + if (mCurr > startId + maxItems-1) + startId = mCurr - maxItems +1; + else if (mCurr < startId) + startId = mCurr; + stars->Update(dt); + selectionT += 3*dt; + selectionY += (selectionTargetY - selectionY) * 8 * dt; + stars->MoveTo( 40 + ((mWidth-2*kHorizontalMargin)*(1+cos(selectionT))/2), selectionY + 5 * cos(selectionT*2.35) + kLineHeight / 2 - kLineHeight * startId); + if (timeOpen < 0) { + timeOpen += dt * 10; + if (timeOpen >= 0) { timeOpen = 0; closed = true; stars->FireAt(mX, mY); } + } else { + closed = false; + timeOpen += dt * 10; + } +} + +void DeckMenu::Add(int id, const char * text,string desc, bool forceFocus, DeckMetaData * deckMetaData) { + DeckMenuItem * menuItem = NEW DeckMenuItem(this, id, fontId, text, 0, mY + kVerticalMargin + mCount*kLineHeight, (mCount == 0), autoTranslate, deckMetaData); + menuItem->desc = deckMetaData ? deckMetaData->getDescription() : desc; + + JGuiController::Add(menuItem); + if (mCount <= maxItems) mHeight += kLineHeight; + if (forceFocus){ + mObjects[mCurr]->Leaving(JGE_BTN_DOWN); + mCurr = mCount-1; + menuItem->Entering(); + } +} + +void DeckMenu::Close() +{ + timeOpen = -1.0; + stars->Stop(true); +} + + +void DeckMenu::destroy(){ + SAFE_DELETE(DeckMenu::stars); +} diff --git a/projects/mtg/src/DeckMenuItem.cpp b/projects/mtg/src/DeckMenuItem.cpp new file mode 100644 index 000000000..420d5ef35 --- /dev/null +++ b/projects/mtg/src/DeckMenuItem.cpp @@ -0,0 +1,109 @@ +#include "PrecompiledHeader.h" + +#include "DeckMenuItem.h" +#include "Translate.h" +#include "WResourceManager.h" + +DeckMenuItem::DeckMenuItem(DeckMenu* _parent, int id, int fontId, string text, int x, int y, bool hasFocus, bool autoTranslate, DeckMetaData *deckMetaData): JGuiObject(id), parent(_parent), fontId(fontId), mX(x), mY(y) +{ + if (autoTranslate) + mText = _(text); + else + mText = text; + mHasFocus = hasFocus; + + mScale = 1.0f; + mTargetScale = 1.0f; + + if (hasFocus) + Entering(); + + meta = deckMetaData; + if ( meta && meta->getAvatarFilename().size() > 0 ) + this->imageFilename = meta->getAvatarFilename(); + else + this->imageFilename = "avatar.jpg"; + +} + + +void DeckMenuItem::RenderWithOffset(float yOffset) +{ + WFont * mFont = resources.GetWFont(fontId); + mFont->DrawString(mText.c_str(), mX, mY + yOffset, JGETEXT_CENTER); +} + +void DeckMenuItem::Render() +{ + RenderWithOffset(0); +} + +void DeckMenuItem::Update(float dt) +{ + if (mScale < mTargetScale) + { + mScale += 8.0f*dt; + if (mScale > mTargetScale) + mScale = mTargetScale; + } + else if (mScale > mTargetScale) + { + mScale -= 8.0f*dt; + if (mScale < mTargetScale) + mScale = mTargetScale; + } +} + + +void DeckMenuItem::Entering() +{ + mHasFocus = true; + parent->selectionTargetY = mY; +} + + +bool DeckMenuItem::Leaving(JButton key) +{ + mHasFocus = false; + return true; +} + + +bool DeckMenuItem::ButtonPressed() +{ + return true; +} + +void DeckMenuItem::Relocate(int x, int y) +{ + mX = x; + mY = y; +} + +int DeckMenuItem::GetWidth() +{ + WFont * mFont = resources.GetWFont(fontId); + mFont->SetScale(1.0); + return mFont->GetStringWidth(mText.c_str()); +} + +bool DeckMenuItem::hasFocus() +{ + return mHasFocus; +} + +ostream& DeckMenuItem::toString(ostream& out) const +{ + return out << "DeckMenuItem ::: mHasFocus : " << mHasFocus + << " ; parent : " << parent + << " ; mText : " << mText + << " ; mScale : " << mScale + << " ; mTargetScale : " << mTargetScale + << " ; mX,mY : " << mX << "," << mY; +} + + +DeckMenuItem::~DeckMenuItem() +{ + meta = NULL; +} \ No newline at end of file diff --git a/projects/mtg/src/DeckMetaData.cpp b/projects/mtg/src/DeckMetaData.cpp index 538c76153..e3c88c501 100644 --- a/projects/mtg/src/DeckMetaData.cpp +++ b/projects/mtg/src/DeckMetaData.cpp @@ -33,6 +33,9 @@ void DeckMetaData::loadStatsForPlayer( Player * statsPlayer, string deckStatsFil _percentVictories = stats->percentVictories(deckStatsFileName); _victories = opponentDeckStats->victories; _nbGamesPlayed = opponentDeckStats->nbgames; + ostringstream oss; + oss << "avatar" << deckStatsFileName.substr( deckStatsFileName.find("deck") + 4, deckStatsFileName.find_last_of(".") -1 ) << ".jpg"; + _avatarFilename = oss.str(); if (_percentVictories < 34) { _difficulty = HARD; @@ -46,6 +49,12 @@ void DeckMetaData::loadStatsForPlayer( Player * statsPlayer, string deckStatsFil _difficulty = EASY; } } + else + { + ostringstream oss; + oss << "avatar" << this->getDeckId() << ".jpg"; + _avatarFilename = oss.str(); + } } else { @@ -114,6 +123,10 @@ int DeckMetaData::getDeckId() return _deckid; } +string DeckMetaData::getAvatarFilename() +{ + return _avatarFilename; +} int DeckMetaData::getGamesPlayed() { @@ -136,10 +149,9 @@ int DeckMetaData::getDifficulty() return _difficulty; } -string DeckMetaData::getDescription() +string DeckMetaData::getDifficultyString() { - char deckDesc[512]; - string difficultyString = ""; + string difficultyString = "Normal"; switch( _difficulty ) { case HARD: @@ -149,12 +161,22 @@ string DeckMetaData::getDescription() difficultyString = "Easy"; break; } - if ( _nbGamesPlayed > 0 && difficultyString != "") - sprintf(deckDesc, "Deck: %s\nDifficulty: %s\nVictory %%: %i\nGames Played: %i\n\n%s", _name.c_str(), difficultyString.c_str(), _percentVictories, _nbGamesPlayed, _desc.c_str() ); - else if ( _nbGamesPlayed > 0 ) - sprintf(deckDesc, "Deck: %s\nVictory %%: %i\nGames Played: %i\n\n%s", _name.c_str(), _percentVictories, _nbGamesPlayed, _desc.c_str() ); - else - sprintf(deckDesc, "Deck: %s\n\n%s", _name.c_str(), _desc.c_str() ); - return deckDesc; + return difficultyString; +} + +string DeckMetaData::getDescription() +{ + return _desc; +} + +string DeckMetaData::getStatsSummary() +{ + ostringstream statsSummary; + statsSummary << "Difficulty: " << getDifficultyString() << endl + << "Victory %: " << getVictoryPercentage() << endl + << "Games Played: " << getGamesPlayed() << endl; + + return statsSummary.str(); + } diff --git a/projects/mtg/src/GameApp.cpp b/projects/mtg/src/GameApp.cpp index f50c82ac9..12b53b1ec 100644 --- a/projects/mtg/src/GameApp.cpp +++ b/projects/mtg/src/GameApp.cpp @@ -242,6 +242,7 @@ void GameApp::Destroy() Translator::EndInstance(); WCFilterFactory::Destroy(); SimpleMenu::destroy(); + DeckMenu::destroy(); options.theGame = NULL; LOG("==Destroying GameApp Successful=="); diff --git a/projects/mtg/src/GameState.cpp b/projects/mtg/src/GameState.cpp index 56cb1467a..c1848886e 100644 --- a/projects/mtg/src/GameState.cpp +++ b/projects/mtg/src/GameState.cpp @@ -23,6 +23,16 @@ vector GameState::fillDeckMenu( SimpleMenu * _menu, string path, return deckMetaDataVector; } +vector GameState::fillDeckMenu( DeckMenu * _menu, string path, string smallDeckPrefix, Player * statsPlayer){ + bool translate = _menu->autoTranslate; + _menu->autoTranslate = false; + vector deckMetaDataVector = getValidDeckMetaData( path, smallDeckPrefix, statsPlayer ); + renderDeckMenu( _menu, deckMetaDataVector); + _menu->autoTranslate = translate; + + return deckMetaDataVector; +} + vector GameState::getValidDeckMetaData( string path, string smallDeckPrefix, Player * statsPlayer) { @@ -91,6 +101,28 @@ void GameState::renderDeckMenu ( SimpleMenu * _menu, vector deck } +// build a menu with the given deck list and return a vector of the deck ids created. +void GameState::renderDeckMenu ( DeckMenu * _menu, vector deckMetaDataList ) +{ + int deckNumber = 1; + Translator * t = Translator::GetInstance(); + map::iterator it; + for (vector::iterator i = deckMetaDataList.begin(); i != deckMetaDataList.end(); i++) + { + DeckMetaData * deckMetaData = *i; + string deckName = deckMetaData -> getName(); + string deckDescription = deckMetaData -> getDescription(); + int deckId = deckMetaData -> getDeckId(); + //translate decks desc + it = t->deckValues.find(deckName); + if (it != t->deckValues.end()) + _menu->Add(deckNumber++, deckName.c_str(), it->second, false, deckMetaData); + else + _menu->Add( deckNumber++ ,deckName.c_str(), deckDescription.c_str(), false, deckMetaData); + } +} + + // deck sorting routines bool sortByName( DeckMetaData * d1, DeckMetaData * d2 ) { diff --git a/projects/mtg/src/GameStateDuel.cpp b/projects/mtg/src/GameStateDuel.cpp index 280d3691d..debf35c72 100644 --- a/projects/mtg/src/GameStateDuel.cpp +++ b/projects/mtg/src/GameStateDuel.cpp @@ -1,5 +1,6 @@ #include "PrecompiledHeader.h" +#include "DeckMenu.h" #include "GameStateDuel.h" #include "GameOptions.h" #include "utils.h" @@ -87,11 +88,11 @@ void GameStateDuel::Start() menu = NULL; int decksneeded = 0; - for (int i = 0; i<2; i ++){ if (mParent->players[i] == PLAYER_TYPE_HUMAN){ decksneeded = 1; - deckmenu = NEW SimpleMenu(DUEL_MENU_CHOOSE_DECK, this, Fonts::MENU_FONT, 35, 25, "Choose a Deck"); + + deckmenu = NEW DeckMenu(DUEL_MENU_CHOOSE_DECK, this, Fonts::MENU_FONT, "Choose a Deck"); DeckManager *deckManager = DeckManager::GetInstance(); vector playerDeckList = getValidDeckMetaData( options.profileFile() ); @@ -196,7 +197,6 @@ void GameStateDuel::loadTestSuitePlayers(){ void GameStateDuel::End() { DebugTrace("Ending GameStateDuel"); - SAFE_DELETE(deckmenu); JRenderer::GetInstance()->EnableVSync(false); if (mPlayers[0] && mPlayers[1]) mPlayers[0]->End(); @@ -215,6 +215,7 @@ void GameStateDuel::End() SAFE_DELETE(menu); SAFE_DELETE(opponentMenu); + SAFE_DELETE(deckmenu); #ifdef TESTSUITE SAFE_DELETE(testSuite); #endif @@ -236,7 +237,7 @@ bool GameStateDuel::MusicExist(string FileName){ void GameStateDuel::ensureOpponentMenu(){ if (!opponentMenu){ - opponentMenu = NEW SimpleMenu(DUEL_MENU_CHOOSE_OPPONENT, this, Fonts::MENU_FONT, 35, 25, "Choose Opponent"); + opponentMenu = NEW DeckMenu(DUEL_MENU_CHOOSE_OPPONENT, this, Fonts::MENU_FONT, "Choose Your Opponent"); opponentMenu->Add( MENUITEM_RANDOM_AI, "Random"); if (options[Options::EVILTWIN_MODE_UNLOCKED].number) opponentMenu->Add( MENUITEM_EVIL_TWIN, "Evil Twin", _("Can you play against yourself?").c_str());