1 /*************************************************************************** 2 * Free Heroes of Might and Magic II: https://github.com/ihhub/fheroes2 * 3 * Copyright (C) 2021 * 4 * * 5 * This program is free software; you can redistribute it and/or modify * 6 * it under the terms of the GNU General Public License as published by * 7 * the Free Software Foundation; either version 2 of the License, or * 8 * (at your option) any later version. * 9 * * 10 * This program is distributed in the hope that it will be useful, * 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of * 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * 13 * GNU General Public License for more details. * 14 * * 15 * You should have received a copy of the GNU General Public License * 16 * along with this program; if not, write to the * 17 * Free Software Foundation, Inc., * 18 * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * 19 ***************************************************************************/ 20 21 #include "dialog_system_options.h" 22 #include "agg_image.h" 23 #include "audio.h" 24 #include "cursor.h" 25 #include "dialog.h" 26 #include "game.h" 27 #include "game_delays.h" 28 #include "game_interface.h" 29 #include "icn.h" 30 #include "localevent.h" 31 #include "screen.h" 32 #include "settings.h" 33 #include "text.h" 34 #include "translations.h" 35 #include "ui_button.h" 36 37 #include <cassert> 38 39 namespace 40 { 41 enum class DialogAction : int 42 { 43 Open, 44 ChangeInterfaceTheme, 45 UpdateInterface, 46 SaveConfiguration, 47 Close 48 }; 49 50 const int textOffset = 2; 51 drawOption(const fheroes2::Rect & optionRoi,const fheroes2::Sprite & icon,const std::string & title,const std::string & value)52 void drawOption( const fheroes2::Rect & optionRoi, const fheroes2::Sprite & icon, const std::string & title, const std::string & value ) 53 { 54 fheroes2::Display & display = fheroes2::Display::instance(); 55 56 fheroes2::Blit( icon, display, optionRoi.x, optionRoi.y ); 57 Text text( title, Font::SMALL ); 58 text.Blit( optionRoi.x + ( optionRoi.width - text.w() ) / 2, optionRoi.y - text.h() - textOffset ); 59 60 text.Set( value ); 61 text.Blit( optionRoi.x + ( optionRoi.width - text.w() ) / 2, optionRoi.y + optionRoi.height + textOffset ); 62 } 63 drawDialog(const std::vector<fheroes2::Rect> & rects)64 void drawDialog( const std::vector<fheroes2::Rect> & rects ) 65 { 66 assert( rects.size() == 9 ); 67 68 const Settings & conf = Settings::Get(); 69 70 // Music volume. 71 const fheroes2::Sprite & musicVolumeIcon = fheroes2::AGG::GetICN( ICN::SPANEL, Audio::isValid() ? 1 : 0 ); 72 std::string value; 73 if ( Audio::isValid() && conf.MusicVolume() ) { 74 value = std::to_string( conf.MusicVolume() ); 75 } 76 else { 77 value = _( "off" ); 78 } 79 80 drawOption( rects[0], musicVolumeIcon, _( "Music" ), value ); 81 82 // Sound volume. 83 const fheroes2::Sprite & soundVolumeOption = fheroes2::AGG::GetICN( ICN::SPANEL, Audio::isValid() ? 3 : 2 ); 84 if ( Audio::isValid() && conf.SoundVolume() ) { 85 value = std::to_string( conf.SoundVolume() ); 86 } 87 else { 88 value = _( "off" ); 89 } 90 91 drawOption( rects[1], soundVolumeOption, _( "Effects" ), value ); 92 93 // Music Type. 94 const MusicSource musicType = conf.MusicType(); 95 const fheroes2::Sprite & musicTypeIcon = fheroes2::AGG::GetICN( ICN::SPANEL, musicType == MUSIC_EXTERNAL ? 11 : 10 ); 96 if ( musicType == MUSIC_MIDI_ORIGINAL ) { 97 value = _( "MIDI" ); 98 } 99 else if ( musicType == MUSIC_MIDI_EXPANSION ) { 100 value = _( "MIDI Expansion" ); 101 } 102 else if ( musicType == MUSIC_EXTERNAL ) { 103 value = _( "External" ); 104 } 105 106 drawOption( rects[2], musicTypeIcon, _( "Music Type" ), value ); 107 108 // Hero's movement speed. 109 const int heroSpeed = conf.HeroesMoveSpeed(); 110 uint32_t heroSpeedIconId = 9; 111 if ( heroSpeed >= 4 ) { 112 heroSpeedIconId = 3 + heroSpeed / 2; 113 } 114 else if ( heroSpeed > 0 ) { 115 heroSpeedIconId = 4; 116 } 117 118 const fheroes2::Sprite & heroSpeedIcon = fheroes2::AGG::GetICN( ICN::SPANEL, heroSpeedIconId ); 119 if ( heroSpeed == 10 ) { 120 value = _( "Jump" ); 121 } 122 else { 123 value = std::to_string( heroSpeed ); 124 } 125 126 drawOption( rects[3], heroSpeedIcon, _( "Hero Speed" ), value ); 127 128 // AI's movement speed. 129 const int aiSpeed = conf.AIMoveSpeed(); 130 uint32_t aiSpeedIconId = 9; 131 if ( aiSpeed >= 4 ) { 132 aiSpeedIconId = 3 + aiSpeed / 2; 133 } 134 else if ( aiSpeed > 0 ) { 135 aiSpeedIconId = 4; 136 } 137 138 const fheroes2::Sprite & aiSpeedIcon = fheroes2::AGG::GetICN( ICN::SPANEL, aiSpeedIconId ); 139 if ( aiSpeed == 0 ) { 140 value = _( "Don't Show" ); 141 } 142 else if ( aiSpeed == 10 ) { 143 value = _( "Jump" ); 144 } 145 else { 146 value = std::to_string( aiSpeed ); 147 } 148 149 drawOption( rects[4], aiSpeedIcon, _( "Enemy Speed" ), value ); 150 151 // Scrolling speed. 152 const int scrollSpeed = conf.ScrollSpeed(); 153 uint32_t scrollSpeedIconId = 7; 154 if ( scrollSpeed < SCROLL_NORMAL ) { 155 scrollSpeedIconId = 4; 156 } 157 else if ( scrollSpeed < SCROLL_FAST1 ) { 158 scrollSpeedIconId = 5; 159 } 160 else if ( scrollSpeed < SCROLL_FAST2 ) { 161 scrollSpeedIconId = 6; 162 } 163 164 const fheroes2::Sprite & scrollSpeedIcon = fheroes2::AGG::GetICN( ICN::SPANEL, scrollSpeedIconId ); 165 drawOption( rects[5], scrollSpeedIcon, _( "Scroll Speed" ), std::to_string( scrollSpeed ) ); 166 167 // Interface theme. 168 const bool isEvilInterface = conf.ExtGameEvilInterface(); 169 const fheroes2::Sprite & interfaceThemeIcon = fheroes2::AGG::GetICN( ICN::SPANEL, isEvilInterface ? 17 : 16 ); 170 if ( isEvilInterface ) { 171 value = _( "Evil" ); 172 } 173 else { 174 value = _( "Good" ); 175 } 176 177 drawOption( rects[6], interfaceThemeIcon, _( "Interface Type" ), value ); 178 179 // Interface show/hide state. 180 const bool isHiddenInterface = conf.ExtGameHideInterface(); 181 const fheroes2::Sprite & interfaceStateIcon = isHiddenInterface ? fheroes2::AGG::GetICN( ICN::ESPANEL, 4 ) : fheroes2::AGG::GetICN( ICN::SPANEL, 16 ); 182 if ( isHiddenInterface ) { 183 value = _( "Hide" ); 184 } 185 else { 186 value = _( "Show" ); 187 } 188 189 drawOption( rects[7], interfaceStateIcon, _( "Interface" ), value ); 190 191 // Auto-battles. 192 if ( conf.BattleAutoResolve() ) { 193 const bool spellcast = conf.BattleAutoSpellcast(); 194 value = spellcast ? _( "Auto Resolve" ) : _( "Auto, No Spells" ); 195 196 const fheroes2::Sprite & autoBattleIcon = fheroes2::AGG::GetICN( ICN::CSPANEL, spellcast ? 7 : 6 ); 197 drawOption( rects[8], autoBattleIcon, _( "Battles" ), value ); 198 } 199 else { 200 const fheroes2::Sprite & autoBattleIcon = fheroes2::AGG::GetICN( ICN::SPANEL, 18 ); 201 drawOption( rects[8], autoBattleIcon, _( "Battles" ), _( "Manual" ) ); 202 } 203 } 204 openSystemOptionsDialog()205 DialogAction openSystemOptionsDialog() 206 { 207 const CursorRestorer cursorRestorer( true, Cursor::POINTER ); 208 209 Settings & conf = Settings::Get(); 210 const bool isEvilInterface = conf.ExtGameEvilInterface(); 211 212 fheroes2::Display & display = fheroes2::Display::instance(); 213 214 const fheroes2::Sprite & dialog = fheroes2::AGG::GetICN( ( isEvilInterface ? ICN::SPANBKGE : ICN::SPANBKG ), 0 ); 215 const fheroes2::Sprite & dialogShadow = fheroes2::AGG::GetICN( ( isEvilInterface ? ICN::SPANBKGE : ICN::SPANBKG ), 1 ); 216 217 const fheroes2::Point dialogOffset( ( display.width() - dialog.width() ) / 2, ( display.height() - dialog.height() ) / 2 ); 218 const fheroes2::Point shadowOffset( dialogOffset.x - BORDERWIDTH, dialogOffset.y ); 219 220 fheroes2::ImageRestorer back( display, shadowOffset.x, shadowOffset.y, dialog.width() + BORDERWIDTH, dialog.height() + BORDERWIDTH ); 221 const fheroes2::Rect dialogArea( dialogOffset.x, dialogOffset.y, dialog.width(), dialog.height() ); 222 223 fheroes2::Fill( display, dialogArea.x, dialogArea.y, dialogArea.width, dialogArea.height, 0 ); 224 fheroes2::Blit( dialogShadow, display, dialogArea.x - BORDERWIDTH, dialogArea.y + BORDERWIDTH ); 225 fheroes2::Blit( dialog, display, dialogArea.x, dialogArea.y ); 226 227 const fheroes2::Sprite & optionSprite = fheroes2::AGG::GetICN( ICN::SPANEL, 0 ); 228 const fheroes2::Point optionOffset( 36 + dialogArea.x, 47 + dialogArea.y ); 229 const fheroes2::Point optionStep( 92, 110 ); 230 231 std::vector<fheroes2::Rect> roi; 232 233 for ( int32_t y = 0; y < 3; ++y ) { 234 for ( int32_t x = 0; x < 3; ++x ) { 235 roi.emplace_back( optionOffset.x + x * optionStep.x, optionOffset.y + y * optionStep.y, optionSprite.width(), optionSprite.height() ); 236 } 237 } 238 239 const fheroes2::Rect & musicVolumeRoi = roi[0]; 240 const fheroes2::Rect & soundVolumeRoi = roi[1]; 241 const fheroes2::Rect & musicTypeRoi = roi[2]; 242 const fheroes2::Rect & heroSpeedRoi = roi[3]; 243 const fheroes2::Rect & aiSpeedRoi = roi[4]; 244 const fheroes2::Rect & scrollSpeedRoi = roi[5]; 245 const fheroes2::Rect & interfaceTypeRoi = roi[6]; 246 const fheroes2::Rect & interfaceStateRoi = roi[7]; 247 const fheroes2::Rect & battleResolveRoi = roi[8]; 248 249 drawDialog( roi ); 250 251 const fheroes2::Point buttonOffset( 112 + dialogArea.x, 362 + dialogArea.y ); 252 fheroes2::Button buttonOkay( buttonOffset.x, buttonOffset.y, isEvilInterface ? ICN::SPANBTNE : ICN::SPANBTN, 0, 1 ); 253 buttonOkay.draw(); 254 255 display.render(); 256 257 bool saveConfig = false; 258 259 // dialog menu loop 260 LocalEvent & le = LocalEvent::Get(); 261 while ( le.HandleEvents() ) { 262 le.MousePressLeft( buttonOkay.area() ) ? buttonOkay.drawOnPress() : buttonOkay.drawOnRelease(); 263 264 if ( le.MouseClickLeft( buttonOkay.area() ) || HotKeyCloseWindow ) { 265 break; 266 } 267 268 // set music or sound volume 269 bool saveMusicVolume = false; 270 bool saveSoundVolume = false; 271 if ( Audio::isValid() ) { 272 if ( le.MouseClickLeft( musicVolumeRoi ) ) { 273 conf.SetMusicVolume( ( conf.MusicVolume() + 1 ) % 11 ); 274 saveMusicVolume = true; 275 } 276 else if ( le.MouseWheelUp( musicVolumeRoi ) ) { 277 conf.SetMusicVolume( conf.MusicVolume() + 1 ); 278 saveMusicVolume = true; 279 } 280 else if ( le.MouseWheelDn( musicVolumeRoi ) ) { 281 conf.SetMusicVolume( conf.MusicVolume() - 1 ); 282 saveMusicVolume = true; 283 } 284 if ( saveMusicVolume ) { 285 Music::Volume( static_cast<int16_t>( Mixer::MaxVolume() * conf.MusicVolume() / 10 ) ); 286 } 287 288 if ( le.MouseClickLeft( soundVolumeRoi ) ) { 289 conf.SetSoundVolume( ( conf.SoundVolume() + 1 ) % 11 ); 290 saveSoundVolume = true; 291 } 292 else if ( le.MouseWheelUp( soundVolumeRoi ) ) { 293 conf.SetSoundVolume( conf.SoundVolume() + 1 ); 294 saveSoundVolume = true; 295 } 296 else if ( le.MouseWheelDn( soundVolumeRoi ) ) { 297 conf.SetSoundVolume( conf.SoundVolume() - 1 ); 298 saveSoundVolume = true; 299 } 300 if ( saveSoundVolume ) { 301 Game::EnvironmentSoundMixer(); 302 } 303 } 304 305 // set music type 306 bool saveMusicType = false; 307 if ( le.MouseClickLeft( musicTypeRoi ) ) { 308 int type = conf.MusicType() + 1; 309 // If there's no expansion files we skip this option 310 if ( type == MUSIC_MIDI_EXPANSION && !conf.isPriceOfLoyaltySupported() ) 311 ++type; 312 313 const Game::MusicRestorer musicRestorer; 314 315 conf.SetMusicType( type > MUSIC_EXTERNAL ? 0 : type ); 316 317 Game::SetCurrentMusic( MUS::UNKNOWN ); 318 319 saveMusicType = true; 320 } 321 322 // set hero speed 323 bool saveHeroSpeed = false; 324 if ( le.MouseClickLeft( heroSpeedRoi ) ) { 325 conf.SetHeroesMoveSpeed( conf.HeroesMoveSpeed() % 10 + 1 ); 326 saveHeroSpeed = true; 327 } 328 else if ( le.MouseWheelUp( heroSpeedRoi ) ) { 329 conf.SetHeroesMoveSpeed( conf.HeroesMoveSpeed() + 1 ); 330 saveHeroSpeed = true; 331 } 332 else if ( le.MouseWheelDn( heroSpeedRoi ) ) { 333 conf.SetHeroesMoveSpeed( conf.HeroesMoveSpeed() - 1 ); 334 saveHeroSpeed = true; 335 } 336 337 // set ai speed 338 bool saveAISpeed = false; 339 if ( le.MouseClickLeft( aiSpeedRoi ) ) { 340 conf.SetAIMoveSpeed( ( conf.AIMoveSpeed() + 1 ) % 11 ); 341 saveAISpeed = true; 342 } 343 else if ( le.MouseWheelUp( aiSpeedRoi ) ) { 344 conf.SetAIMoveSpeed( conf.AIMoveSpeed() + 1 ); 345 saveAISpeed = true; 346 } 347 else if ( le.MouseWheelDn( aiSpeedRoi ) ) { 348 conf.SetAIMoveSpeed( conf.AIMoveSpeed() - 1 ); 349 saveAISpeed = true; 350 } 351 352 if ( saveHeroSpeed || saveAISpeed ) { 353 Game::UpdateGameSpeed(); 354 } 355 356 // set scroll speed 357 bool saveScrollSpeed = false; 358 if ( le.MouseClickLeft( scrollSpeedRoi ) ) { 359 conf.SetScrollSpeed( conf.ScrollSpeed() % SCROLL_FAST2 + 1 ); 360 saveScrollSpeed = true; 361 } 362 else if ( le.MouseWheelUp( scrollSpeedRoi ) ) { 363 conf.SetScrollSpeed( conf.ScrollSpeed() + 1 ); 364 saveScrollSpeed = true; 365 } 366 else if ( le.MouseWheelDn( scrollSpeedRoi ) ) { 367 conf.SetScrollSpeed( conf.ScrollSpeed() - 1 ); 368 saveScrollSpeed = true; 369 } 370 371 // set interface theme 372 if ( le.MouseClickLeft( interfaceTypeRoi ) ) { 373 return DialogAction::ChangeInterfaceTheme; 374 } 375 376 // set interface hide/show 377 if ( le.MouseClickLeft( interfaceStateRoi ) ) { 378 return DialogAction::UpdateInterface; 379 } 380 381 // toggle manual/auto battles 382 bool saveAutoBattle = false; 383 if ( le.MouseClickLeft( battleResolveRoi ) ) { 384 if ( conf.BattleAutoResolve() ) { 385 if ( conf.BattleAutoSpellcast() ) { 386 conf.setBattleAutoSpellcast( false ); 387 } 388 else { 389 conf.setBattleAutoResolve( false ); 390 } 391 } 392 else { 393 conf.setBattleAutoResolve( true ); 394 conf.setBattleAutoSpellcast( true ); 395 } 396 saveAutoBattle = true; 397 } 398 399 if ( le.MousePressRight( musicVolumeRoi ) ) 400 Dialog::Message( _( "Music" ), _( "Toggle ambient music level." ), Font::BIG ); 401 else if ( le.MousePressRight( soundVolumeRoi ) ) 402 Dialog::Message( _( "Effects" ), _( "Toggle foreground sounds level." ), Font::BIG ); 403 else if ( le.MousePressRight( musicTypeRoi ) ) 404 Dialog::Message( _( "Music Type" ), _( "Change the type of music." ), Font::BIG ); 405 else if ( le.MousePressRight( heroSpeedRoi ) ) 406 Dialog::Message( _( "Hero Speed" ), _( "Change the speed at which your heroes move on the main screen." ), Font::BIG ); 407 else if ( le.MousePressRight( aiSpeedRoi ) ) 408 Dialog::Message( _( "Enemy Speed" ), _( "Sets the speed that A.I. heroes move at. You can also elect not to view A.I. movement at all." ), Font::BIG ); 409 else if ( le.MousePressRight( scrollSpeedRoi ) ) 410 Dialog::Message( _( "Scroll Speed" ), _( "Sets the speed at which you scroll the window." ), Font::BIG ); 411 else if ( le.MousePressRight( interfaceTypeRoi ) ) 412 Dialog::Message( _( "Interface Type" ), _( "Toggle the type of interface you want to use." ), Font::BIG ); 413 else if ( le.MousePressRight( interfaceStateRoi ) ) 414 Dialog::Message( _( "Interface" ), _( "Toggle interface visibility." ), Font::BIG ); 415 else if ( le.MousePressRight( battleResolveRoi ) ) 416 Dialog::Message( _( "Battles" ), _( "Toggle instant battle mode." ), Font::BIG ); 417 else if ( le.MousePressRight( buttonOkay.area() ) ) 418 Dialog::Message( _( "OK" ), _( "Exit this menu." ), Font::BIG ); 419 420 if ( saveMusicVolume || saveSoundVolume || saveMusicType || saveHeroSpeed || saveAISpeed || saveScrollSpeed || saveAutoBattle ) { 421 // redraw 422 fheroes2::Blit( dialog, display, dialogArea.x, dialogArea.y ); 423 drawDialog( roi ); 424 buttonOkay.draw(); 425 display.render(); 426 427 saveConfig = true; 428 } 429 } 430 431 if ( saveConfig ) { 432 return DialogAction::SaveConfiguration; 433 } 434 435 return DialogAction::Close; 436 } 437 } 438 439 namespace fheroes2 440 { showSystemOptionsDialog()441 void showSystemOptionsDialog() 442 { 443 // We should make file writing only once. 444 bool saveConfiguration = false; 445 446 DialogAction action = DialogAction::Open; 447 448 while ( action != DialogAction::Close ) { 449 switch ( action ) { 450 case DialogAction::Open: 451 action = openSystemOptionsDialog(); 452 break; 453 case DialogAction::ChangeInterfaceTheme: { 454 Settings & conf = Settings::Get(); 455 conf.SetEvilInterface( !conf.ExtGameEvilInterface() ); 456 saveConfiguration = true; 457 458 Interface::Basic & basicInterface = Interface::Basic::Get(); 459 Interface::GameArea & gamearea = basicInterface.GetGameArea(); 460 const fheroes2::Point prevCenter = gamearea.getCurrentCenterInPixels(); 461 462 basicInterface.Reset(); 463 gamearea.SetCenterInPixels( prevCenter ); 464 basicInterface.Redraw( Interface::REDRAW_ALL ); 465 466 action = openSystemOptionsDialog(); 467 break; 468 } 469 case DialogAction::UpdateInterface: { 470 Settings & conf = Settings::Get(); 471 conf.SetHideInterface( !conf.ExtGameHideInterface() ); 472 saveConfiguration = true; 473 474 Interface::Basic & basicInterface = Interface::Basic::Get(); 475 Interface::GameArea & gamearea = basicInterface.GetGameArea(); 476 const fheroes2::Point prevCenter = gamearea.getCurrentCenterInPixels(); 477 const fheroes2::Rect prevRoi = gamearea.GetROI(); 478 479 basicInterface.SetHideInterface( conf.ExtGameHideInterface() ); 480 481 basicInterface.Reset(); 482 483 const fheroes2::Rect newRoi = gamearea.GetROI(); 484 485 gamearea.SetCenterInPixels( prevCenter + fheroes2::Point( newRoi.width / 2, newRoi.height / 2 ) + fheroes2::Point( newRoi.x, newRoi.y ) 486 - fheroes2::Point( prevRoi.width / 2, prevRoi.height / 2 ) - fheroes2::Point( prevRoi.x, prevRoi.y ) ); 487 488 // We need to redraw radar first due to the nature of restorers. Only then we can redraw everything. 489 basicInterface.Redraw( Interface::REDRAW_RADAR ); 490 basicInterface.Redraw( Interface::REDRAW_ALL ); 491 492 action = openSystemOptionsDialog(); 493 break; 494 } 495 case DialogAction::SaveConfiguration: 496 Settings::Get().Save( "fheroes2.cfg" ); 497 return; 498 default: 499 break; 500 } 501 } 502 503 if ( saveConfiguration ) { 504 Settings::Get().Save( "fheroes2.cfg" ); 505 } 506 } 507 } 508